@programinglive/dev-workflow-mcp-server 1.13.16 → 1.14.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/AGENTS.md +20 -8
- package/README.md +38 -3
- package/netlify.toml +20 -3
- package/package.json +1 -1
- 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
|
|
@@ -617,7 +647,12 @@ Since 30 October 2026 the reliable fix is **per task**, not global. Pass
|
|
|
617
647
|
git operation uses it:
|
|
618
648
|
|
|
619
649
|
```json
|
|
620
|
-
{
|
|
650
|
+
{
|
|
651
|
+
"description": "Fix login redirect",
|
|
652
|
+
"type": "bugfix",
|
|
653
|
+
"projectDir": "D:\\code\\clinic",
|
|
654
|
+
"definitionOfDone": ["redirect kembali ke halaman asal", "build lulus"]
|
|
655
|
+
}
|
|
621
656
|
```
|
|
622
657
|
|
|
623
658
|
`start_task` answers with `📁 Repo proyek: <path> (<source>)`, so the repo in use
|
package/netlify.toml
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
|
+
# Deploy for devworkflow.programinglive.com
|
|
2
|
+
#
|
|
3
|
+
# The site is a Next.js **static export** (`output: "export"` in
|
|
4
|
+
# web/next.config.ts), so `next build` writes plain HTML/JS into `web/out` and
|
|
5
|
+
# there is no server runtime at all.
|
|
6
|
+
#
|
|
7
|
+
# Two things this file has to get right, because both have silently broken the
|
|
8
|
+
# deploy before:
|
|
9
|
+
#
|
|
10
|
+
# 1. `publish` must point at `out`, relative to `base`. With the line removed
|
|
11
|
+
# ("auto-detection"), Netlify could not find the export and kept serving an
|
|
12
|
+
# old successful deploy while every new build looked fine.
|
|
13
|
+
# 2. `@netlify/plugin-nextjs` must NOT be loaded. It is for server-rendered
|
|
14
|
+
# Next.js and expects `.next`; with a static export it only adds ways for
|
|
15
|
+
# the build to fail.
|
|
1
16
|
[build]
|
|
2
17
|
base = "web"
|
|
3
18
|
command = "npm run build"
|
|
4
|
-
|
|
19
|
+
publish = "out"
|
|
5
20
|
|
|
6
|
-
[
|
|
7
|
-
|
|
21
|
+
[build.environment]
|
|
22
|
+
# Next.js 16 requires Node 20.9+. Pin it so a Netlify default bump cannot
|
|
23
|
+
# change the build under us.
|
|
24
|
+
NODE_VERSION = "22"
|
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.1",
|
|
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/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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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\
|
|
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
|
|
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
|
|
package/workflow-state.js
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { assertSafeKnowledge } from './security/policy.js';
|
|
6
6
|
import { sanitize } from './security/sanitizer.js';
|
|
7
|
+
import { normalizeItems as normalizeDodItems, mergeDefinitionOfDone as mergeDodItems } from './tools/handler-modules/dod.js';
|
|
7
8
|
import { fileURLToPath } from "url";
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -658,6 +659,14 @@ export class WorkflowState {
|
|
|
658
659
|
projectDir: "",
|
|
659
660
|
featureFlowCreated: false,
|
|
660
661
|
featureFlowDescription: "",
|
|
662
|
+
// Definition of Done milik task ini. Aturan dan bentuk itemnya ada di
|
|
663
|
+
// tools/handler-modules/dod.js; di sini hanya disimpan.
|
|
664
|
+
definitionOfDone: [],
|
|
665
|
+
dodSource: "",
|
|
666
|
+
dodFactoryTaskId: "",
|
|
667
|
+
// Naik setiap ada perubahan kerja baru setelah commit tercatat, sehingga
|
|
668
|
+
// bukti item `done` yang lama otomatis tidak lagi berlaku.
|
|
669
|
+
dodRevision: 0,
|
|
661
670
|
history: [],
|
|
662
671
|
taskType: "",
|
|
663
672
|
};
|
|
@@ -702,6 +711,14 @@ export class WorkflowState {
|
|
|
702
711
|
projectDir: "",
|
|
703
712
|
featureFlowCreated: false,
|
|
704
713
|
featureFlowDescription: "",
|
|
714
|
+
// Definition of Done milik task ini. Aturan dan bentuk itemnya ada di
|
|
715
|
+
// tools/handler-modules/dod.js; di sini hanya disimpan.
|
|
716
|
+
definitionOfDone: [],
|
|
717
|
+
dodSource: "",
|
|
718
|
+
dodFactoryTaskId: "",
|
|
719
|
+
// Naik setiap ada perubahan kerja baru setelah commit tercatat, sehingga
|
|
720
|
+
// bukti item `done` yang lama otomatis tidak lagi berlaku.
|
|
721
|
+
dodRevision: 0,
|
|
705
722
|
history: [],
|
|
706
723
|
taskType: "",
|
|
707
724
|
};
|
|
@@ -768,6 +785,21 @@ export class WorkflowState {
|
|
|
768
785
|
if (typeof this.state.featureFlowDescription !== "string") {
|
|
769
786
|
this.state.featureFlowDescription = "";
|
|
770
787
|
}
|
|
788
|
+
// Berkas state yang ditulis sebelum fitur DoD ada tidak punya field ini.
|
|
789
|
+
// Ia harus muncul sebagai "belum punya DoD", bukan undefined yang membuat
|
|
790
|
+
// gerbang melempar TypeError.
|
|
791
|
+
this.state.definitionOfDone = normalizeDodItems(this.state.definitionOfDone);
|
|
792
|
+
if (typeof this.state.dodSource !== "string") {
|
|
793
|
+
this.state.dodSource = "";
|
|
794
|
+
}
|
|
795
|
+
if (typeof this.state.dodFactoryTaskId !== "string") {
|
|
796
|
+
this.state.dodFactoryTaskId = "";
|
|
797
|
+
}
|
|
798
|
+
if (!Number.isFinite(Number(this.state.dodRevision)) || Number(this.state.dodRevision) < 0) {
|
|
799
|
+
this.state.dodRevision = 0;
|
|
800
|
+
} else {
|
|
801
|
+
this.state.dodRevision = Math.trunc(Number(this.state.dodRevision));
|
|
802
|
+
}
|
|
771
803
|
} catch (error) {
|
|
772
804
|
if (error?.code !== "ENOENT") {
|
|
773
805
|
throw error;
|
|
@@ -887,6 +919,48 @@ export class WorkflowState {
|
|
|
887
919
|
}
|
|
888
920
|
}
|
|
889
921
|
|
|
922
|
+
/**
|
|
923
|
+
* Gabungkan item DoD dengan milik disk bila task-nya sama.
|
|
924
|
+
*
|
|
925
|
+
* Sama seperti `mergeMonotonicProgress`, ini hanya berlaku untuk task yang
|
|
926
|
+
* SAMA. `start_task` mengganti task, dan pada saat itu DoD memang harus mulai
|
|
927
|
+
* dari nol - bukan mewarisi bukti task sebelumnya.
|
|
928
|
+
*
|
|
929
|
+
* Bedanya, DoD digabung per item dengan aturan "yang lebih kuat menang",
|
|
930
|
+
* bukan OR boolean: dua proses MCP bisa melayani task yang sama, dan proses
|
|
931
|
+
* yang memegang state basi tidak boleh menghapus bukti item yang baru dicatat
|
|
932
|
+
* proses lain.
|
|
933
|
+
*/
|
|
934
|
+
mergeDefinitionOfDone(reconciled) {
|
|
935
|
+
const diskTask = (reconciled?.diskTask || "").trim();
|
|
936
|
+
const ourTask = (this.state.taskDescription || "").trim();
|
|
937
|
+
if (!diskTask || !ourTask || diskTask !== ourTask) {
|
|
938
|
+
return 0;
|
|
939
|
+
}
|
|
940
|
+
if ((reconciled?.diskPhase || "idle") === "idle") {
|
|
941
|
+
return 0;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const disk = reconciled?.diskState;
|
|
945
|
+
if (!disk) {
|
|
946
|
+
return 0;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const before = Array.isArray(this.state.definitionOfDone) ? this.state.definitionOfDone : [];
|
|
950
|
+
const merged = mergeDodItems(before, normalizeDodItems(disk.definitionOfDone));
|
|
951
|
+
this.state.definitionOfDone = merged;
|
|
952
|
+
|
|
953
|
+
// Revisi hanya boleh maju: revisi yang lebih tinggi berarti ada perubahan
|
|
954
|
+
// kerja baru yang sudah diketahui proses lain. Mundur berarti bukti basi
|
|
955
|
+
// diam-diam dihidupkan kembali.
|
|
956
|
+
const diskRevision = Number(disk.dodRevision);
|
|
957
|
+
if (Number.isFinite(diskRevision) && diskRevision > Number(this.state.dodRevision || 0)) {
|
|
958
|
+
this.state.dodRevision = Math.trunc(diskRevision);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return merged.length - before.length;
|
|
962
|
+
}
|
|
963
|
+
|
|
890
964
|
async save(options = {}) {
|
|
891
965
|
assertSafeKnowledge(this.state);
|
|
892
966
|
const reconciled = await this.reconcileWithDisk();
|
|
@@ -918,6 +992,7 @@ export class WorkflowState {
|
|
|
918
992
|
// disk memang task yang sama. `start_task` mengganti task, dan pada saat itu
|
|
919
993
|
// flag harus benar-benar direset, bukan dihidupkan kembali.
|
|
920
994
|
this.mergeMonotonicProgress(reconciled);
|
|
995
|
+
this.mergeDefinitionOfDone(reconciled);
|
|
921
996
|
|
|
922
997
|
this.state.owner = {
|
|
923
998
|
processMarker: PROCESS_TOKEN,
|
|
@@ -1272,6 +1347,14 @@ export class WorkflowState {
|
|
|
1272
1347
|
projectDir: "",
|
|
1273
1348
|
featureFlowCreated: false,
|
|
1274
1349
|
featureFlowDescription: "",
|
|
1350
|
+
// Definition of Done milik task ini. Aturan dan bentuk itemnya ada di
|
|
1351
|
+
// tools/handler-modules/dod.js; di sini hanya disimpan.
|
|
1352
|
+
definitionOfDone: [],
|
|
1353
|
+
dodSource: "",
|
|
1354
|
+
dodFactoryTaskId: "",
|
|
1355
|
+
// Naik setiap ada perubahan kerja baru setelah commit tercatat, sehingga
|
|
1356
|
+
// bukti item `done` yang lama otomatis tidak lagi berlaku.
|
|
1357
|
+
dodRevision: 0,
|
|
1275
1358
|
history,
|
|
1276
1359
|
taskType: "",
|
|
1277
1360
|
};
|