@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.
- package/AGENTS.md +20 -8
- package/README.md +32 -2
- package/index.js +20 -15
- package/package.json +1 -1
- package/process-lock.js +63 -6
- package/tools/definitions.js +80 -1
- package/tools/handler-modules/commit-handlers.js +53 -1
- package/tools/handler-modules/dod-handlers.js +217 -0
- package/tools/handler-modules/dod.js +335 -0
- package/tools/handler-modules/factory-handlers.js +27 -1
- package/tools/handler-modules/index.js +7 -0
- package/tools/handler-modules/shared.js +5 -0
- package/tools/handler-modules/status-handlers.js +61 -3
- package/tools/handler-modules/workflow-handlers.js +35 -1
- package/workflow-state.js +83 -0
package/AGENTS.md
CHANGED
|
@@ -7,16 +7,28 @@ You are connected to the `dev-workflow` MCP server. **You MUST NOT perform any c
|
|
|
7
7
|
### The Rule of Operation
|
|
8
8
|
Before taking any action or answering user requests regarding code changes, check your workflow status and follow these exact steps in sequence:
|
|
9
9
|
|
|
10
|
-
1. **Start Task**: Call `start_task` with a clear description
|
|
10
|
+
1. **Start Task**: Call `start_task` with a clear description, type (`feature`, `bugfix`, `refactor`, or `other`), `projectDir`, **and a `definitionOfDone` checklist**. Every task must say, up front, which concrete conditions must be true before it counts as finished.
|
|
11
11
|
2. **Implement**: Write your code changes.
|
|
12
12
|
3. **Mark Fixed**: Call `mark_bug_fixed` with a summary of the implementation.
|
|
13
|
-
4. **
|
|
14
|
-
5. **
|
|
15
|
-
6. **
|
|
16
|
-
7. **
|
|
17
|
-
8. **
|
|
18
|
-
9. **
|
|
19
|
-
10. **
|
|
13
|
+
4. **Prove the Definition of Done**: For each item, call `mark_dod_item` with `evidence` (5-500 characters naming what you actually ran or checked, e.g. `npm test -> 505 pass`). An item that turned out not to apply is excluded with `waive_dod_item` and a written reason - never silently dropped.
|
|
14
|
+
5. **Create Tests**: Create appropriate unit/integration tests covering your changes.
|
|
15
|
+
6. **Run Tests**: Execute your test suite and call `run_tests` with `passed: true` and the test command. **Never commit if tests fail!**
|
|
16
|
+
7. **Document**: Update documentation (README, PRD, release notes) and call `create_documentation`.
|
|
17
|
+
8. **Ready Check**: Call `check_ready_to_commit` to verify all steps. It fails while any definition-of-done item is still open.
|
|
18
|
+
9. **Commit & Push**: Call `commit_and_push` with a conventional commit message. It refuses while any item is open.
|
|
19
|
+
10. **Release**: Call `perform_release` with the release command (or `skip_release` if non-Node/docs-only).
|
|
20
|
+
11. **Complete**: Call `complete_task` to finish the session and reset the workflow. It refuses while any item is open, and records `dodSummary` in history.
|
|
21
|
+
|
|
22
|
+
### Definition of Done
|
|
23
|
+
|
|
24
|
+
`start_task` **requires** `definitionOfDone`: 1-20 items, each 3-300 characters, no duplicates. The workflow will not let you commit or close a task while an item is still open.
|
|
25
|
+
|
|
26
|
+
- `mark_dod_item { id, evidence }` proves an item. Re-marking a proven item refreshes its evidence.
|
|
27
|
+
- `waive_dod_item { id, reason }` excludes an item with a written reason. A proven item cannot be waived.
|
|
28
|
+
- `set_definition_of_done { items, append }` fills the checklist of a task started before this rule existed. Replacing a list that already has proven items is refused unless `append: true`.
|
|
29
|
+
- When new working changes appear after a commit, `dodRevision` goes up and previously proven items must be re-confirmed against the code that is actually being committed. Waivers survive.
|
|
30
|
+
|
|
31
|
+
`force_complete_task` remains the audited escape hatch: it records `forced: true` with your reason and the DoD summary.
|
|
20
32
|
|
|
21
33
|
### Quick Shortcuts
|
|
22
34
|
- If you have all information ready, you can use `run_full_workflow` to execute all steps safely in sequence.
|
package/README.md
CHANGED
|
@@ -110,11 +110,14 @@ See [MCP client configuration](#mcp-client-configuration).
|
|
|
110
110
|
|
|
111
111
|
## Available Tools
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
25 tools.
|
|
114
114
|
|
|
115
115
|
| Tool | Purpose |
|
|
116
116
|
|---|---|
|
|
117
|
-
| `start_task` | Begin a task (always the first step) |
|
|
117
|
+
| `start_task` | Begin a task (always the first step); requires `definitionOfDone` |
|
|
118
|
+
| `set_definition_of_done` | Add or replace the checklist of the current task |
|
|
119
|
+
| `mark_dod_item` | Prove one item, with evidence |
|
|
120
|
+
| `waive_dod_item` | Exclude one item, with a reason |
|
|
118
121
|
| `create_feature_flow` | Record the feature flow as Mermaid |
|
|
119
122
|
| `mark_bug_fixed` | Mark the implementation done |
|
|
120
123
|
| `create_tests` | Mark tests as written |
|
|
@@ -142,6 +145,33 @@ See [MCP client configuration](#mcp-client-configuration).
|
|
|
142
145
|
| `claim_factory_task` | Claim a Factory task and start a local workflow |
|
|
143
146
|
| `sync_task_status` | Report a status change back to Factory |
|
|
144
147
|
|
|
148
|
+
### Definition of Done
|
|
149
|
+
|
|
150
|
+
Every task carries a checklist of the concrete conditions that must be true before it counts as finished. `start_task` requires it:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"description": "Perbaiki validasi form appointment",
|
|
155
|
+
"type": "bugfix",
|
|
156
|
+
"projectDir": "D:\\code\\clinic",
|
|
157
|
+
"definitionOfDone": ["build lulus", "validasi menolak input kosong"]
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Each item is later either **proven** or **excluded**, and the workflow refuses to commit or close the task while any item is still open:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{ "id": "dod-1", "evidence": "php artisan test -> 412 pass" }
|
|
165
|
+
{ "id": "dod-2", "reason": "form ini tidak punya jalur input kosong, sudah dicek di controller" }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- Evidence is required (5-500 characters). It is what separates "proven" from "claimed".
|
|
169
|
+
- An excluded item keeps its reason in the state and in the `history` entry's `definitionOfDoneSummary`.
|
|
170
|
+
- When new working changes appear after a commit, `dodRevision` increments and previously proven items must be re-confirmed against the code that is actually being committed. Waivers survive.
|
|
171
|
+
- A task claimed from the Factory board imports the board checklist as **open** items: a checked box on the board is someone's plan, not proof that this machine ran anything.
|
|
172
|
+
- `set_definition_of_done` fills the checklist of a task started before this rule existed. Replacing a list that already has proven items is refused unless `append: true`.
|
|
173
|
+
- `force_complete_task` is the audited escape hatch: `forced: true`, your reason, and the DoD summary land in history.
|
|
174
|
+
|
|
145
175
|
### Factory task integration
|
|
146
176
|
|
|
147
177
|
Five tools connect this server to the **Factory** board at
|
package/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
15
15
|
import { fileURLToPath } from "url";
|
|
16
16
|
import path from "path";
|
|
17
|
-
import { acquireProcessLock, releaseProcessLock, lockWarning } from "./process-lock.js";
|
|
17
|
+
import { acquireProcessLock, releaseProcessLock, lockWarning, projectLockDir } from "./process-lock.js";
|
|
18
18
|
import { exec, setActiveProjectDir } from "./exec.js";
|
|
19
19
|
import { normalizeRequestArgs, shellEscape } from "./utils.js";
|
|
20
20
|
import { createGitFacade } from "./git-facade.js";
|
|
@@ -199,26 +199,31 @@ export async function main() {
|
|
|
199
199
|
// Select request metadata before the first disk write. Creating an idle
|
|
200
200
|
// PID fallback here leaves a phantom session alongside the real sessions.
|
|
201
201
|
safeLog('error', `[${new Date().toISOString()}] Workflow state initialization complete.`);
|
|
202
|
-
|
|
203
|
-
// Penanda proses: 31 Oktober 2026 dua proses MCP hidup bersamaan dan
|
|
204
|
-
// memegang kredensial Factory yang berbeda, sehingga tool yang sama
|
|
205
|
-
// menjawab berbeda tergantung proses mana yang melayaninya. Proses lain
|
|
206
|
-
// TIDAK dibunuh (itu berbahaya); cukup dilaporkan supaya pengguna tahu
|
|
207
|
-
// harus menutup OpenCode dan membukanya lagi.
|
|
208
|
-
try {
|
|
209
|
-
const stateDir = path.dirname(path.resolve(workflowState.stateFile));
|
|
210
|
-
const warning = lockWarning(acquireProcessLock({ dir: stateDir }));
|
|
211
|
-
if (warning) safeLog('error', warning);
|
|
212
|
-
process.once('exit', () => releaseProcessLock({ dir: stateDir }));
|
|
213
|
-
} catch (lockError) {
|
|
214
|
-
safeLog('error', 'Warning: could not record process lock:', lockError.message);
|
|
215
|
-
}
|
|
216
202
|
} catch (error) {
|
|
217
203
|
safeLog('error', `[${new Date().toISOString()}] Warning: Failed to initialize workflow state:`, error.message);
|
|
218
204
|
// Continue anyway - workflow state is optional for MCP to function
|
|
219
205
|
workflowState = new WorkflowState();
|
|
220
206
|
}
|
|
221
207
|
|
|
208
|
+
// Penanda proses - DI LUAR try di atas, dengan sengaja.
|
|
209
|
+
//
|
|
210
|
+
// 31 Oktober 2026: blok ini semula berada di dalam try yang sama dengan
|
|
211
|
+
// inisialisasi state. Saat dua proses MCP start serempak, satu di antaranya
|
|
212
|
+
// gagal menginisialisasi state (berebut berkas yang sama), masuk ke catch, dan
|
|
213
|
+
// penanda prosesnya tidak pernah ditulis. Hasilnya dua proses hidup dengan
|
|
214
|
+
// satu lock file: proses ganda tidak pernah terdeteksi.
|
|
215
|
+
//
|
|
216
|
+
// Lokasinya juga harus di `.state` PROYEK, bukan di direktori user: direktori
|
|
217
|
+
// user bisa berbeda antar proses, sehingga keduanya tidak saling melihat.
|
|
218
|
+
try {
|
|
219
|
+
const lockDir = projectLockDir(workflowState.stateFile);
|
|
220
|
+
const warning = lockWarning(acquireProcessLock({ dir: lockDir }));
|
|
221
|
+
if (warning) safeLog('error', warning);
|
|
222
|
+
process.once('exit', () => releaseProcessLock({ dir: lockDir }));
|
|
223
|
+
} catch (lockError) {
|
|
224
|
+
safeLog('error', 'Warning: could not record process lock:', lockError.message);
|
|
225
|
+
}
|
|
226
|
+
|
|
222
227
|
const transport = new StdioServerTransport();
|
|
223
228
|
// Cover SDK-generated protocol errors as well as tool-handler responses.
|
|
224
229
|
const send = transport.send.bind(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@programinglive/dev-workflow-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "MCP server to enforce development workflow discipline",
|
|
5
5
|
"homepage": "https://devworkflow.programinglive.com/",
|
|
6
6
|
"mcpName": "io.github.programinglive/dev-workflow-mcp-server",
|
package/process-lock.js
CHANGED
|
@@ -24,6 +24,41 @@ export function processLockPath({ dir }) {
|
|
|
24
24
|
return path.join(dir, LOCK_FILENAME);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Direktori `.state` milik PROYEK untuk sebuah berkas state.
|
|
29
|
+
*
|
|
30
|
+
* Kenapa bukan `path.dirname(stateFile)` saja: berkas state sesi berada di
|
|
31
|
+
* `.state/users/<user>/`, dan direktori user bisa BERBEDA antar proses (id user
|
|
32
|
+
* dibuat per direktori state). Pada 31 Oktober 2026 dua proses MCP hidup
|
|
33
|
+
* bersamaan tetapi hanya satu lock file yang tercipta, karena masing-masing
|
|
34
|
+
* menulis ke direktori user-nya sendiri - jadi keduanya tidak pernah saling
|
|
35
|
+
* melihat dan peringatan proses ganda tidak muncul.
|
|
36
|
+
*
|
|
37
|
+
* Lock harus satu per PROYEK, bukan per user. Fungsi ini naik sampai menemukan
|
|
38
|
+
* direktori bernama `.state` dan memakai direktori itu. Kalau tidak ada (state
|
|
39
|
+
* di luar tata letak itu), pakai direktori berkasnya sendiri.
|
|
40
|
+
*/
|
|
41
|
+
export function projectLockDir(stateFilePath) {
|
|
42
|
+
const resolved = path.resolve(stateFilePath);
|
|
43
|
+
let current = path.dirname(resolved);
|
|
44
|
+
const { root } = path.parse(current);
|
|
45
|
+
|
|
46
|
+
while (true) {
|
|
47
|
+
if (path.basename(current) === ".state") {
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const parent = path.dirname(current);
|
|
52
|
+
if (parent === current || current === root) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
current = parent;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return path.dirname(resolved);
|
|
60
|
+
}
|
|
61
|
+
|
|
27
62
|
/**
|
|
28
63
|
* Membaca isi lock, atau null bila tidak ada / tidak terbaca.
|
|
29
64
|
*
|
|
@@ -66,7 +101,13 @@ function defaultIsAlive(pid) {
|
|
|
66
101
|
* { acquired: true, tookOverFrom: <pid> } -> pemegang lama sudah mati
|
|
67
102
|
* { acquired: false, holder: {...} } -> ada proses lain yang hidup
|
|
68
103
|
*/
|
|
69
|
-
export function acquireProcessLock({
|
|
104
|
+
export function acquireProcessLock({
|
|
105
|
+
dir,
|
|
106
|
+
pid = process.pid,
|
|
107
|
+
now = Date.now(),
|
|
108
|
+
isAlive = defaultIsAlive,
|
|
109
|
+
writeFileImpl = writeFileSync,
|
|
110
|
+
}) {
|
|
70
111
|
const existing = readProcessLock({ dir });
|
|
71
112
|
|
|
72
113
|
if (existing && existing.pid !== pid && isAlive(existing.pid)) {
|
|
@@ -79,10 +120,17 @@ export function acquireProcessLock({ dir, pid = process.pid, now = Date.now(), i
|
|
|
79
120
|
|
|
80
121
|
try {
|
|
81
122
|
mkdirSync(dir, { recursive: true });
|
|
82
|
-
|
|
83
|
-
} catch {
|
|
84
|
-
// Tidak bisa menulis penanda bukan alasan mematikan server
|
|
85
|
-
|
|
123
|
+
writeFileImpl(processLockPath({ dir }), JSON.stringify(payload), "utf8");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
// Tidak bisa menulis penanda bukan alasan mematikan server, TETAPI juga
|
|
126
|
+
// tidak boleh ditelan: sampai 31 Oktober 2026 kegagalan ini hanya
|
|
127
|
+
// mengembalikan { acquired: true } sehingga sistem tampak sehat padahal
|
|
128
|
+
// tidak ada penanda apa pun di disk. Simpan sebabnya supaya bisa dilaporkan.
|
|
129
|
+
return {
|
|
130
|
+
acquired: true,
|
|
131
|
+
wrote: false,
|
|
132
|
+
error: (error && (error.code || error.message)) || "unknown",
|
|
133
|
+
};
|
|
86
134
|
}
|
|
87
135
|
|
|
88
136
|
return tookOverFrom ? { acquired: true, tookOverFrom } : { acquired: true };
|
|
@@ -118,7 +166,16 @@ function safeHostname() {
|
|
|
118
166
|
* nyata, dan supaya pemanggil bebas memilih tidak menampilkan apa pun.
|
|
119
167
|
*/
|
|
120
168
|
export function lockWarning(result) {
|
|
121
|
-
if (!result
|
|
169
|
+
if (!result) return null;
|
|
170
|
+
|
|
171
|
+
if (result.wrote === false) {
|
|
172
|
+
return (
|
|
173
|
+
`Tidak bisa menulis penanda proses ke ${LOCK_FILENAME} (${result.error}). ` +
|
|
174
|
+
"Deteksi proses MCP ganda tidak akan bekerja sampai ini diperbaiki."
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (result.acquired !== false || !result.holder) return null;
|
|
122
179
|
|
|
123
180
|
const started = Number.isFinite(result.holder.startedAt)
|
|
124
181
|
? new Date(result.holder.startedAt).toISOString()
|
package/tools/definitions.js
CHANGED
|
@@ -33,8 +33,87 @@ export function getToolList() {
|
|
|
33
33
|
"Pass it whenever the session directory is not itself a git repository, " +
|
|
34
34
|
"otherwise the server may fall back to WORKFLOW_PROJECT_DIR and commit to the wrong repo.",
|
|
35
35
|
},
|
|
36
|
+
definitionOfDone: {
|
|
37
|
+
type: "array",
|
|
38
|
+
items: { type: "string" },
|
|
39
|
+
minItems: 1,
|
|
40
|
+
maxItems: 20,
|
|
41
|
+
description:
|
|
42
|
+
"REQUIRED. The concrete conditions that must be true before this task counts as done, " +
|
|
43
|
+
"one condition per item, for example ['build lulus', 'halaman login terverifikasi']. " +
|
|
44
|
+
"Each item must later be proven with 'mark_dod_item' (evidence required) or waived with " +
|
|
45
|
+
"'waive_dod_item' (reason required). A task cannot be committed or closed while any item is open.",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ["description", "type", "definitionOfDone"],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "set_definition_of_done",
|
|
53
|
+
description:
|
|
54
|
+
"Add or replace the definition-of-done checklist of the CURRENT task. Use it to fill the checklist of a task started before this feature existed, because a task cannot be committed or closed while any item is open. Replacing a list that already contains proven or waived items is refused unless 'append' is true. Items that are not proven must be waived with a written reason instead of quietly dropped.",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
items: {
|
|
59
|
+
type: "array",
|
|
60
|
+
items: { type: "string" },
|
|
61
|
+
minItems: 1,
|
|
62
|
+
maxItems: 20,
|
|
63
|
+
description:
|
|
64
|
+
"Conditions that must be true before the task is done, 3-300 characters each, for example ['build lulus', 'halaman login terverifikasi'].",
|
|
65
|
+
},
|
|
66
|
+
append: {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
description:
|
|
69
|
+
"When true, add the items to the existing checklist instead of replacing it. Required when the current checklist already has proven or waived items.",
|
|
70
|
+
},
|
|
71
|
+
reason: {
|
|
72
|
+
type: "string",
|
|
73
|
+
description: "Optional note recorded with the change.",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: ["items"],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "mark_dod_item",
|
|
81
|
+
description:
|
|
82
|
+
"Mark one definition-of-done item as proven, with the evidence that proves it. Evidence is required: 5-500 characters naming what was actually run or checked, for example 'npm test -> 482 pass'. Marking an already-proven item again is allowed and refreshes its evidence, which is how a stale item is re-confirmed after new changes. Waived items cannot be marked; reinstate them with set_definition_of_done { append: true }.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
id: {
|
|
87
|
+
type: "string",
|
|
88
|
+
description: "Item id from start_task, get_workflow_status, or set_definition_of_done (for example 'dod-2').",
|
|
89
|
+
},
|
|
90
|
+
evidence: {
|
|
91
|
+
type: "string",
|
|
92
|
+
description:
|
|
93
|
+
"What proves the condition is true, 5-500 characters. Name the command and its result, or the page and what you saw.",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
required: ["id", "evidence"],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "waive_dod_item",
|
|
101
|
+
description:
|
|
102
|
+
"Exclude one definition-of-done item with a written reason, for work that turned out not to be needed for this task. The reason is required (5-500 characters) and is recorded in the state and in the history entry, so it must be readable by someone else. A proven item cannot be waived.",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
id: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "Item id from start_task or get_workflow_status (for example 'dod-3').",
|
|
109
|
+
},
|
|
110
|
+
reason: {
|
|
111
|
+
type: "string",
|
|
112
|
+
description:
|
|
113
|
+
"Why this condition does not apply to this task, 5-500 characters. Do not use it to avoid work that is actually required.",
|
|
114
|
+
},
|
|
36
115
|
},
|
|
37
|
-
required: ["
|
|
116
|
+
required: ["id", "reason"],
|
|
38
117
|
},
|
|
39
118
|
},
|
|
40
119
|
{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { textResponse, isReleaseSatisfied, formatChecklist } from "./shared.js";
|
|
2
2
|
import { handleReadyCheck } from "./status-handlers.js";
|
|
3
|
+
import { openItems, renderChecklist, renderOutstanding, summarize } from "./dod.js";
|
|
3
4
|
import { containsTestFilesInStatus } from "../../git-helpers.js";
|
|
4
5
|
import { resolveProjectDir, setActiveProjectDir, getActiveProjectDir } from "../../exec.js";
|
|
5
6
|
import path from "path";
|
|
@@ -122,6 +123,23 @@ export async function handleCommitAndPush(args, context) {
|
|
|
122
123
|
|
|
123
124
|
workflowState.state.projectDir = getActiveProjectDir() || workflowState.state.projectDir || "";
|
|
124
125
|
|
|
126
|
+
// Guard DoD yang eksplisit. `handleReadyCheck` di bawah sudah menutupi ini
|
|
127
|
+
// secara transitif, tetapi pesannya generik ("NOT READY"); di sini kita bisa
|
|
128
|
+
// menyebut item mana yang menghalangi, sebelum satu perintah git pun jalan.
|
|
129
|
+
const dodItems = Array.isArray(workflowState.state.definitionOfDone)
|
|
130
|
+
? workflowState.state.definitionOfDone
|
|
131
|
+
: [];
|
|
132
|
+
const outstanding = openItems(dodItems, workflowState.state.dodRevision);
|
|
133
|
+
if (outstanding.length > 0) {
|
|
134
|
+
return textResponse(
|
|
135
|
+
`⚠️ Dibatalkan: ${outstanding.length} item Definition of Done belum terpenuhi.\n\n` +
|
|
136
|
+
`${renderOutstanding(dodItems, workflowState.state.dodRevision)}\n\n` +
|
|
137
|
+
`Buktikan dengan 'mark_dod_item' { id, evidence }, atau kecualikan dengan ` +
|
|
138
|
+
`'waive_dod_item' { id, reason }.\n\n` +
|
|
139
|
+
`Tidak ada yang di-stage dan tidak ada yang di-commit.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
125
143
|
if (!workflowState.state.readyToCommit || !workflowState.state.readyCheckCompleted) {
|
|
126
144
|
const readyResponse = await handleReadyCheck(workflowState);
|
|
127
145
|
if (workflowState.state.currentPhase !== "commit") {
|
|
@@ -302,10 +320,35 @@ export async function handleCommitAndPush(args, context) {
|
|
|
302
320
|
}
|
|
303
321
|
|
|
304
322
|
export async function handleCompleteTask(args, workflowState) {
|
|
323
|
+
// Sampai 31 Oktober 2026 fungsi ini tidak memeriksa apa pun: task bisa
|
|
324
|
+
// ditutup dengan DoD kosong atau masih penuh item terbuka. Sekarang tidak.
|
|
325
|
+
const dodState = workflowState.state;
|
|
326
|
+
const dodItems = Array.isArray(dodState.definitionOfDone) ? dodState.definitionOfDone : [];
|
|
327
|
+
const dod = summarize(dodItems, dodState.dodRevision);
|
|
328
|
+
|
|
329
|
+
if (dodItems.length === 0) {
|
|
330
|
+
return textResponse(
|
|
331
|
+
`⚠️ Task ini belum punya Definition of Done, jadi belum bisa ditutup.\n\n` +
|
|
332
|
+
`Isi dulu dengan 'set_definition_of_done' { items: [...] }, buktikan tiap item, lalu ulangi 'complete_task'.\n` +
|
|
333
|
+
`Kalau memang harus ditutup sekarang, pakai 'force_complete_task' dengan alasan - langkah itu tercatat.`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (dod.open > 0) {
|
|
338
|
+
const outstanding = openItems(dodItems, dodState.dodRevision);
|
|
339
|
+
return textResponse(
|
|
340
|
+
`⚠️ ${dod.open} item Definition of Done masih terbuka:\n\n` +
|
|
341
|
+
`${renderOutstanding(dodItems, dodState.dodRevision)}\n\n` +
|
|
342
|
+
`Buktikan dengan 'mark_dod_item' { id, evidence } atau kecualikan dengan ` +
|
|
343
|
+
`'waive_dod_item' { id, reason }.`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
305
347
|
const entry = {
|
|
306
348
|
taskDescription: workflowState.state.taskDescription,
|
|
307
349
|
taskType: workflowState.state.taskType,
|
|
308
350
|
commitMessage: args.commitMessage,
|
|
351
|
+
definitionOfDoneSummary: dod,
|
|
309
352
|
};
|
|
310
353
|
|
|
311
354
|
workflowState.addToHistory(entry);
|
|
@@ -313,7 +356,8 @@ export async function handleCompleteTask(args, workflowState) {
|
|
|
313
356
|
await workflowState.save();
|
|
314
357
|
|
|
315
358
|
return textResponse(
|
|
316
|
-
`✅ Task completed!\n\nCommit message: ${args.commitMessage}\
|
|
359
|
+
`✅ Task completed!\n\nCommit message: ${args.commitMessage}\n` +
|
|
360
|
+
`DoD: ${dod.done}/${dod.total} terbukti, ${dod.waived} dikecualikan.\nHistory recorded.`
|
|
317
361
|
);
|
|
318
362
|
}
|
|
319
363
|
|
|
@@ -331,6 +375,10 @@ export async function handleForceCompleteTask(args, workflowState) {
|
|
|
331
375
|
commitMessage,
|
|
332
376
|
forced: true,
|
|
333
377
|
forceReason: reason,
|
|
378
|
+
definitionOfDoneSummary: summarize(
|
|
379
|
+
Array.isArray(workflowState.state.definitionOfDone) ? workflowState.state.definitionOfDone : [],
|
|
380
|
+
workflowState.state.dodRevision,
|
|
381
|
+
),
|
|
334
382
|
});
|
|
335
383
|
|
|
336
384
|
workflowState.reset();
|
|
@@ -350,6 +398,10 @@ export async function handleDropTask(args, workflowState) {
|
|
|
350
398
|
commitMessage: workflowState.state.lastCommitMessage,
|
|
351
399
|
dropped: true,
|
|
352
400
|
dropReason: typeof args.reason === "string" ? args.reason : "",
|
|
401
|
+
definitionOfDoneSummary: summarize(
|
|
402
|
+
Array.isArray(workflowState.state.definitionOfDone) ? workflowState.state.definitionOfDone : [],
|
|
403
|
+
workflowState.state.dodRevision,
|
|
404
|
+
),
|
|
353
405
|
});
|
|
354
406
|
workflowState.reset();
|
|
355
407
|
await workflowState.save();
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler untuk Definition of Done.
|
|
3
|
+
*
|
|
4
|
+
* Tipis dengan sengaja: aturan "apa yang sah" hidup di `dod.js`, dan handler di
|
|
5
|
+
* sini hanya memvalidasi, mengubah state, lalu menyimpan. Kalau logika aturan
|
|
6
|
+
* ikut masuk ke sini, ia hanya bisa diuji lewat MCP penuh.
|
|
7
|
+
*/
|
|
8
|
+
import { textResponse } from "./shared.js";
|
|
9
|
+
import {
|
|
10
|
+
MAX_ITEMS,
|
|
11
|
+
MAX_EVIDENCE,
|
|
12
|
+
MIN_EVIDENCE,
|
|
13
|
+
validateItemTexts,
|
|
14
|
+
nextItemId,
|
|
15
|
+
renderChecklist,
|
|
16
|
+
} from "./dod.js";
|
|
17
|
+
|
|
18
|
+
function noActiveTask() {
|
|
19
|
+
return textResponse("⚠️ No active task. Use 'start_task' first, or 'set_definition_of_done' after starting one.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findItem(state, id) {
|
|
23
|
+
const items = Array.isArray(state.definitionOfDone) ? state.definitionOfDone : [];
|
|
24
|
+
return items.find((item) => item.id === id) || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function itemList(state) {
|
|
28
|
+
const items = Array.isArray(state.definitionOfDone) ? state.definitionOfDone : [];
|
|
29
|
+
return items.map((item) => ` ${item.id} [${item.status}]: ${item.text}`).join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function touch(workflowState) {
|
|
33
|
+
workflowState.state.readyCheckCompleted = false;
|
|
34
|
+
workflowState.state.readyToCommit = false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function newItem(text, state) {
|
|
38
|
+
return {
|
|
39
|
+
id: nextItemId(state.definitionOfDone || []),
|
|
40
|
+
text,
|
|
41
|
+
status: "open",
|
|
42
|
+
evidence: "",
|
|
43
|
+
waivedReason: "",
|
|
44
|
+
revision: Number(state.dodRevision) || 0,
|
|
45
|
+
updatedAt: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function handleSetDefinitionOfDone(args, workflowState) {
|
|
50
|
+
const state = workflowState.state;
|
|
51
|
+
if (!state || state.currentPhase === "idle") {
|
|
52
|
+
return noActiveTask();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const check = validateItemTexts(args.items);
|
|
56
|
+
if (!check.ok) {
|
|
57
|
+
return textResponse(
|
|
58
|
+
`⚠️ ${check.error}\n\n` +
|
|
59
|
+
`Contoh:\n items: ["build lulus", "halaman login terverifikasi"]`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const existing = Array.isArray(state.definitionOfDone) ? state.definitionOfDone : [];
|
|
64
|
+
const append = args.append === true;
|
|
65
|
+
|
|
66
|
+
// Mengganti seluruh daftar saat sudah ada bukti berarti membuang bukti yang
|
|
67
|
+
// mungkin sudah diperiksa manusia. Itu tidak boleh terjadi diam-diam.
|
|
68
|
+
if (!append) {
|
|
69
|
+
const proven = existing.filter((item) => item.status === "done" || item.status === "waived");
|
|
70
|
+
if (proven.length > 0) {
|
|
71
|
+
return textResponse(
|
|
72
|
+
`⚠️ Daftar ini sudah punya ${proven.length} item yang terbukti atau dikecualikan:\n${itemList(state)}\n\n` +
|
|
73
|
+
`Mengganti seluruh daftar akan membuang bukti itu. Pilih salah satu:\n` +
|
|
74
|
+
` - tambahkan item baru: set_definition_of_done { items: [...], append: true }\n` +
|
|
75
|
+
` - mulai ulang workflow: rerun_workflow`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const total = (append ? existing.length : 0) + check.items.length;
|
|
81
|
+
if (total > MAX_ITEMS) {
|
|
82
|
+
return textResponse(
|
|
83
|
+
`⚠️ Total item DoD akan menjadi ${total}, melebihi batas ${MAX_ITEMS}. ` +
|
|
84
|
+
`Ringkas kondisi yang sejenis sebelum menambah.`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Duplikat terhadap item yang SUDAH ada juga harus ditolak: dua item dengan
|
|
89
|
+
// teks sama hanya membuat gerbang bertanya dua kali.
|
|
90
|
+
const seen = new Set((append ? existing : []).map((item) => item.text.toLowerCase()));
|
|
91
|
+
for (const text of check.items) {
|
|
92
|
+
if (seen.has(text.toLowerCase())) {
|
|
93
|
+
return textResponse(
|
|
94
|
+
`⚠️ Item "${text}" sudah ada di daftar DoD task ini. Hapus duplikatnya dari permintaan.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
seen.add(text.toLowerCase());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const items = append ? existing.slice() : [];
|
|
101
|
+
for (const text of check.items) {
|
|
102
|
+
items.push(newItem(text, { ...state, definitionOfDone: items }));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
state.definitionOfDone = items;
|
|
106
|
+
if (state.dodSource !== "factory") {
|
|
107
|
+
state.dodSource = "manual";
|
|
108
|
+
}
|
|
109
|
+
touch(workflowState);
|
|
110
|
+
await workflowState.save();
|
|
111
|
+
|
|
112
|
+
return textResponse(
|
|
113
|
+
`✅ Definition of Done ${append ? "ditambah" : "disusun"}: ${items.length} item.\n\n` +
|
|
114
|
+
renderChecklist(items, state.dodRevision) +
|
|
115
|
+
`\n\nBuktikan dengan 'mark_dod_item' { id, evidence } atau kecualikan dengan 'waive_dod_item' { id, reason }.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function handleMarkDodItem(args, workflowState) {
|
|
120
|
+
const state = workflowState.state;
|
|
121
|
+
if (!state || state.currentPhase === "idle") {
|
|
122
|
+
return noActiveTask();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
126
|
+
if (!id) {
|
|
127
|
+
return textResponse(`⚠️ Sebutkan 'id' item DoD. Yang tersedia:\n${itemList(state)}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const item = findItem(state, id);
|
|
131
|
+
if (!item) {
|
|
132
|
+
return textResponse(`⚠️ Tidak ada item DoD dengan id "${id}". Yang tersedia:\n${itemList(state)}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (item.status === "waived") {
|
|
136
|
+
return textResponse(
|
|
137
|
+
`⚠️ Item "${item.text}" sudah dikecualikan (${item.waivedReason}).\n` +
|
|
138
|
+
`Kalau sekarang benar-benar perlu dibuktikan, tambahkan sebagai item baru dengan ` +
|
|
139
|
+
`set_definition_of_done { items: [...], append: true }.`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const evidence = typeof args.evidence === "string" ? args.evidence.trim() : "";
|
|
144
|
+
if (evidence.length < MIN_EVIDENCE || evidence.length > MAX_EVIDENCE) {
|
|
145
|
+
return textResponse(
|
|
146
|
+
`⚠️ 'evidence' untuk "${item.text}" harus ${MIN_EVIDENCE}-${MAX_EVIDENCE} karakter dan menyebut buktinya, ` +
|
|
147
|
+
`contoh: "npm test → 482 pass".\n` +
|
|
148
|
+
`Bukti adalah yang membuat item ini berbeda dari sekadar klaim.`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
item.status = "done";
|
|
153
|
+
item.evidence = evidence;
|
|
154
|
+
item.waivedReason = "";
|
|
155
|
+
item.revision = Number(state.dodRevision) || 0;
|
|
156
|
+
item.updatedAt = new Date().toISOString();
|
|
157
|
+
|
|
158
|
+
touch(workflowState);
|
|
159
|
+
await workflowState.save();
|
|
160
|
+
|
|
161
|
+
const remaining = state.definitionOfDone.filter((candidate) => candidate.status === "open");
|
|
162
|
+
|
|
163
|
+
return textResponse(
|
|
164
|
+
`✅ Item "${item.text}" terbukti: ${evidence}\n\n` +
|
|
165
|
+
renderChecklist(state.definitionOfDone, state.dodRevision) +
|
|
166
|
+
(remaining.length === 0
|
|
167
|
+
? `\n\nSemua item DoD sudah terpenuhi.`
|
|
168
|
+
: `\n\nMasih ${remaining.length} item terbuka: ${remaining.map((candidate) => candidate.id).join(", ")}.`),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function handleWaiveDodItem(args, workflowState) {
|
|
173
|
+
const state = workflowState.state;
|
|
174
|
+
if (!state || state.currentPhase === "idle") {
|
|
175
|
+
return noActiveTask();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
179
|
+
if (!id) {
|
|
180
|
+
return textResponse(`⚠️ Sebutkan 'id' item DoD. Yang tersedia:\n${itemList(state)}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const item = findItem(state, id);
|
|
184
|
+
if (!item) {
|
|
185
|
+
return textResponse(`⚠️ Tidak ada item DoD dengan id "${id}". Yang tersedia:\n${itemList(state)}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (item.status === "done") {
|
|
189
|
+
return textResponse(
|
|
190
|
+
`⚠️ Item "${item.text}" sudah terbukti (${item.evidence}); tidak ada yang perlu dikecualikan.`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const reason = typeof args.reason === "string" ? args.reason.trim() : "";
|
|
195
|
+
if (reason.length < MIN_EVIDENCE || reason.length > MAX_EVIDENCE) {
|
|
196
|
+
return textResponse(
|
|
197
|
+
`⚠️ 'reason' untuk "${item.text}" harus ${MIN_EVIDENCE}-${MAX_EVIDENCE} karakter dan menjelaskan ` +
|
|
198
|
+
`mengapa kondisi ini tidak berlaku untuk task ini.\n` +
|
|
199
|
+
`Alasan ini dicatat di state dan di history, jadi tulis yang bisa dibaca orang lain.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
item.status = "waived";
|
|
204
|
+
item.waivedReason = reason;
|
|
205
|
+
item.evidence = "";
|
|
206
|
+
item.revision = Number(state.dodRevision) || 0;
|
|
207
|
+
item.updatedAt = new Date().toISOString();
|
|
208
|
+
|
|
209
|
+
touch(workflowState);
|
|
210
|
+
await workflowState.save();
|
|
211
|
+
|
|
212
|
+
return textResponse(
|
|
213
|
+
`⚠️ Item "${item.text}" dikecualikan: ${reason}\n\n` +
|
|
214
|
+
renderChecklist(state.definitionOfDone, state.dodRevision) +
|
|
215
|
+
`\n\nPengecualian tercatat dan akan muncul di history saat task ditutup.`,
|
|
216
|
+
);
|
|
217
|
+
}
|