@agilsee/mcp-orchestrator 0.5.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.
Files changed (43) hide show
  1. package/bin/cli.js +490 -0
  2. package/dist/index.js +454 -0
  3. package/dist/memory/memory-manager.js +234 -0
  4. package/dist/server/web-server.js +574 -0
  5. package/dist/tools/aggregate-patterns.js +101 -0
  6. package/dist/tools/analyze-history.js +213 -0
  7. package/dist/tools/auto-dispatch.js +199 -0
  8. package/dist/tools/check-energy.js +49 -0
  9. package/dist/tools/cross-search.js +171 -0
  10. package/dist/tools/get-focus.js +7 -0
  11. package/dist/tools/get-identity.js +7 -0
  12. package/dist/tools/get-project-status.js +35 -0
  13. package/dist/tools/list-projects.js +21 -0
  14. package/dist/tools/list-recent-tasks.js +59 -0
  15. package/dist/tools/log-insight.js +43 -0
  16. package/dist/tools/qcc-create.js +82 -0
  17. package/dist/tools/qcc-status.js +164 -0
  18. package/dist/tools/qcc-update.js +188 -0
  19. package/dist/tools/smart-bootstrap.js +255 -0
  20. package/dist/tools/summarize-session.js +161 -0
  21. package/dist/tools/switch-focus.js +40 -0
  22. package/dist/tools/workflow-router.js +438 -0
  23. package/package.json +44 -0
  24. package/templates/index.ts.template +42 -0
  25. package/templates/shared/get-claude-md.ts +12 -0
  26. package/templates/shared/get-current-state.ts +21 -0
  27. package/templates/shared/get-mistakes.ts +18 -0
  28. package/templates/shared/log-task.ts +27 -0
  29. package/templates/shared/predict-impact.ts +67 -0
  30. package/templates/shared/record-mistake.ts +40 -0
  31. package/templates/shared/update-state.ts +83 -0
  32. package/templates/stacks/express/config.json +9 -0
  33. package/templates/stacks/express/list-routes.ts +56 -0
  34. package/templates/stacks/express/symbol-index.ts +70 -0
  35. package/templates/stacks/laravel/config.json +9 -0
  36. package/templates/stacks/laravel/list-routes.ts +19 -0
  37. package/templates/stacks/laravel/symbol-index.ts +64 -0
  38. package/templates/stacks/nextjs/config.json +9 -0
  39. package/templates/stacks/nextjs/list-routes.ts +67 -0
  40. package/templates/stacks/nextjs/symbol-index.ts +78 -0
  41. package/templates/stacks/react/config.json +10 -0
  42. package/templates/stacks/react/list-routes.ts +44 -0
  43. package/templates/stacks/react/symbol-index.ts +81 -0
@@ -0,0 +1,21 @@
1
+ import { readFile } from "fs/promises";
2
+ import { join } from "path";
3
+ export async function listProjects(claudeHome) {
4
+ const path = join(claudeHome, "project-registry.json");
5
+ const raw = await readFile(path, "utf-8");
6
+ const registry = JSON.parse(raw);
7
+ const projects = Object.entries(registry.projects ?? {}).map(([slug, info]) => ({
8
+ slug,
9
+ name: info.name,
10
+ stack_profile: info.stack_profile,
11
+ stack: info.stack,
12
+ status: info.status,
13
+ group: info.group,
14
+ path: info.path,
15
+ }));
16
+ return {
17
+ count: projects.length,
18
+ projects,
19
+ groups: registry.groups,
20
+ };
21
+ }
@@ -0,0 +1,59 @@
1
+ import { readFile, readdir } from "fs/promises";
2
+ import { join } from "path";
3
+ export async function listRecentTasks(claudeHome, slug, limit) {
4
+ const docsRoot = join(claudeHome, "project-docs");
5
+ let slugs;
6
+ if (slug) {
7
+ slugs = [slug];
8
+ }
9
+ else {
10
+ const entries = await readdir(docsRoot, { withFileTypes: true });
11
+ slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
12
+ }
13
+ const allEntries = [];
14
+ for (const s of slugs) {
15
+ try {
16
+ const path = join(docsRoot, s, "AUDIT_LOG.md");
17
+ const content = await readFile(path, "utf-8");
18
+ allEntries.push(...parseAuditLog(content, s));
19
+ }
20
+ catch {
21
+ // skip missing files
22
+ }
23
+ }
24
+ allEntries.sort((a, b) => b.date.localeCompare(a.date));
25
+ return {
26
+ query: { slug: slug ?? "all", limit },
27
+ total_found: allEntries.length,
28
+ entries: allEntries.slice(0, limit),
29
+ };
30
+ }
31
+ function parseAuditLog(content, slug) {
32
+ const entries = [];
33
+ const lines = content.split("\n");
34
+ for (const line of lines) {
35
+ if (!line.startsWith("|"))
36
+ continue;
37
+ if (line.includes("---"))
38
+ continue;
39
+ if (/\|\s*Date\s*\|/i.test(line))
40
+ continue;
41
+ const cols = line
42
+ .split("|")
43
+ .map((c) => c.trim())
44
+ .filter((c, i, arr) => !(i === 0 && c === "") && !(i === arr.length - 1 && c === ""));
45
+ if (cols.length < 2)
46
+ continue;
47
+ if (!/^\d{4}-\d{2}-\d{2}/.test(cols[0]))
48
+ continue;
49
+ entries.push({
50
+ project: slug,
51
+ date: cols[0] ?? "",
52
+ action: cols[1] ?? "",
53
+ files: cols[2] ?? "",
54
+ tokens: cols[3] ?? "",
55
+ result: cols[4] ?? "",
56
+ });
57
+ }
58
+ return entries;
59
+ }
@@ -0,0 +1,43 @@
1
+ import { readFile, writeFile } from "fs/promises";
2
+ import { join } from "path";
3
+ export async function logInsight(claudeHome, args) {
4
+ const path = join(claudeHome, "project-docs", "INSIGHTS.md");
5
+ let content;
6
+ try {
7
+ content = await readFile(path, "utf-8");
8
+ }
9
+ catch {
10
+ content = [
11
+ "# Cross-Project Insights",
12
+ "",
13
+ "> Pattern, feedback, dan gotcha yang berlaku lintas project.",
14
+ "> Auto-updated oleh orchestrator.",
15
+ "",
16
+ ].join("\n");
17
+ }
18
+ const today = new Date().toISOString().slice(0, 10);
19
+ const projectList = args.projects.join(", ");
20
+ const typeLabel = {
21
+ cross_project_pattern: "Pattern",
22
+ feedback: "Feedback",
23
+ workflow: "Workflow",
24
+ gotcha: "Gotcha",
25
+ }[args.type];
26
+ const entry = [
27
+ `## [${today}] [${typeLabel}] ${args.title}`,
28
+ `- **Projects**: ${projectList}`,
29
+ `- **Detail**: ${args.detail}`,
30
+ args.source ? `- **Source**: ${args.source}` : null,
31
+ "",
32
+ ]
33
+ .filter(Boolean)
34
+ .join("\n");
35
+ content = content.trimEnd() + "\n\n" + entry + "\n";
36
+ await writeFile(path, content, "utf-8");
37
+ return {
38
+ file: path,
39
+ type: args.type,
40
+ title: args.title,
41
+ message: `Insight logged: [${typeLabel}] ${args.title}`,
42
+ };
43
+ }
@@ -0,0 +1,82 @@
1
+ import { readFile, writeFile, mkdir } from "fs/promises";
2
+ import { join } from "path";
3
+ export async function qccCreate(claudeHome, args) {
4
+ const docsPath = join(claudeHome, "project-docs", args.slug);
5
+ const qccPath = join(docsPath, "QCC_CYCLES.md");
6
+ // Ensure directory exists
7
+ await mkdir(docsPath, { recursive: true });
8
+ let content;
9
+ try {
10
+ content = await readFile(qccPath, "utf-8");
11
+ }
12
+ catch {
13
+ content = "# QCC Cycles\n\n> Quality Control Circle — 8 Langkah Problem Solving\n";
14
+ }
15
+ // Auto-increment QCC ID
16
+ const idMatches = content.match(/\[QCC-(\d+)\]/g) ?? [];
17
+ const maxId = idMatches.reduce((max, m) => {
18
+ const num = parseInt(m.match(/\d+/)[0], 10);
19
+ return num > max ? num : max;
20
+ }, 0);
21
+ const newId = `QCC-${String(maxId + 1).padStart(3, "0")}`;
22
+ const today = new Date().toISOString().slice(0, 10);
23
+ const cycleBlock = [
24
+ "",
25
+ "---",
26
+ "",
27
+ `## [${newId}] Tema: ${args.theme}`,
28
+ `- **Status**: active`,
29
+ `- **Created**: ${today}`,
30
+ `- **Focus Target**: ${args.focus_target}`,
31
+ "",
32
+ `### Step 2: Kondisi Saat Ini`,
33
+ "(belum diisi)",
34
+ "",
35
+ `### Step 3: Target SMART`,
36
+ "- **S (Specific/Masalah)**: —",
37
+ "- **M (Measurable/Target)**: —",
38
+ "- **A (Achievable/Percepat)**: —",
39
+ "- **R (Relevant/Alasan)**: —",
40
+ "- **T (Time-bound/Waktu)**: —",
41
+ "",
42
+ `### Step 4: Fishbone (Sebab-Akibat)`,
43
+ "```mermaid",
44
+ "graph LR",
45
+ ` ROOT["${args.theme}"]`,
46
+ ` ROOT --- MAN["Man"]`,
47
+ ` ROOT --- MACHINE["Machine"]`,
48
+ ` ROOT --- METHOD["Method"]`,
49
+ ` ROOT --- MATERIAL["Material"]`,
50
+ ` ROOT --- ENV["Environment"]`,
51
+ "```",
52
+ "",
53
+ `### Step 5: Rencana Penanggulangan`,
54
+ "| No | Sebab | Tindakan | PIC | Target |",
55
+ "|----|-------|----------|-----|--------|",
56
+ "",
57
+ `### Step 6: Pelaksanaan`,
58
+ "| No | Tindakan | Status | Tanggal | Catatan |",
59
+ "|----|----------|--------|---------|---------|",
60
+ "",
61
+ `### Step 7: Evaluasi`,
62
+ "- **Before**: —",
63
+ "- **After**: —",
64
+ "- **Target tercapai?**: —",
65
+ "",
66
+ `### Step 8: Validasi`,
67
+ "- **Validated by**: —",
68
+ "- **Date**: —",
69
+ "- **Standardisasi**: —",
70
+ "- **Horizontal deploy**: —",
71
+ "",
72
+ ].join("\n");
73
+ content = content.trimEnd() + "\n" + cycleBlock;
74
+ await writeFile(qccPath, content, "utf-8");
75
+ return {
76
+ qcc_id: newId,
77
+ file: qccPath,
78
+ theme: args.theme,
79
+ focus_target: args.focus_target,
80
+ message: `QCC cycle ${newId} created: "${args.theme}"`,
81
+ };
82
+ }
@@ -0,0 +1,164 @@
1
+ import { readFile, readdir } from "fs/promises";
2
+ import { join } from "path";
3
+ export async function qccStatus(claudeHome, slug, qccId) {
4
+ const docsRoot = join(claudeHome, "project-docs");
5
+ // If slug specified, return for that project only
6
+ if (slug) {
7
+ return await getProjectQccStatus(docsRoot, slug, qccId);
8
+ }
9
+ // Otherwise scan all projects
10
+ let slugs;
11
+ try {
12
+ const entries = await readdir(docsRoot, { withFileTypes: true });
13
+ slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
14
+ }
15
+ catch {
16
+ return [];
17
+ }
18
+ const results = [];
19
+ for (const s of slugs) {
20
+ try {
21
+ const result = await getProjectQccStatus(docsRoot, s);
22
+ if (result.total_cycles > 0) {
23
+ results.push(result);
24
+ }
25
+ }
26
+ catch { /* skip */ }
27
+ }
28
+ return results;
29
+ }
30
+ async function getProjectQccStatus(docsRoot, slug, filterQccId) {
31
+ const qccPath = join(docsRoot, slug, "QCC_CYCLES.md");
32
+ let content;
33
+ try {
34
+ content = await readFile(qccPath, "utf-8");
35
+ }
36
+ catch {
37
+ return { slug, total_cycles: 0, active: 0, completed: 0, cycles: [] };
38
+ }
39
+ // Parse cycles
40
+ const cycleBlocks = content.split(/(?=\n## \[QCC-)/);
41
+ const cycles = [];
42
+ for (const block of cycleBlocks) {
43
+ const headerMatch = block.match(/## \[(QCC-\d+)\] Tema: (.+)/);
44
+ if (!headerMatch)
45
+ continue;
46
+ const qccId = headerMatch[1];
47
+ const theme = headerMatch[2].trim();
48
+ if (filterQccId && qccId !== filterQccId)
49
+ continue;
50
+ const statusMatch = block.match(/\*\*Status\*\*: (\w+)/);
51
+ const createdMatch = block.match(/\*\*Created\*\*: ([\d-]+)/);
52
+ const focusMatch = block.match(/\*\*Focus Target\*\*: (.+)/);
53
+ const status = statusMatch ? statusMatch[1] : "unknown";
54
+ const created = createdMatch ? createdMatch[1] : "unknown";
55
+ const focusTarget = focusMatch ? focusMatch[1].trim() : "—";
56
+ // Check which steps are done
57
+ const stepsDone = [];
58
+ const stepsPending = [];
59
+ // Step 2: check if "(belum diisi)" is replaced
60
+ if (block.includes("### Step 2:")) {
61
+ const step2Section = extractSection(block, "### Step 2:", "### Step 3:");
62
+ if (!step2Section.includes("(belum diisi)") && step2Section.trim().length > 5) {
63
+ stepsDone.push(2);
64
+ }
65
+ else {
66
+ stepsPending.push(2);
67
+ }
68
+ }
69
+ // Step 3: check if SMART fields are filled
70
+ if (block.includes("### Step 3:")) {
71
+ const step3Section = extractSection(block, "### Step 3:", "### Step 4:");
72
+ const dashCount = (step3Section.match(/: —/g) ?? []).length;
73
+ if (dashCount === 0) {
74
+ stepsDone.push(3);
75
+ }
76
+ else {
77
+ stepsPending.push(3);
78
+ }
79
+ }
80
+ // Step 4: check if fishbone has items beyond template
81
+ if (block.includes("### Step 4:")) {
82
+ const step4Section = extractSection(block, "### Step 4:", "### Step 5:");
83
+ // Count node connections (more than 5 base connections = has items)
84
+ const connections = (step4Section.match(/---/g) ?? []).length;
85
+ if (connections > 5) {
86
+ stepsDone.push(4);
87
+ }
88
+ else {
89
+ stepsPending.push(4);
90
+ }
91
+ }
92
+ // Step 5: check if countermeasure table has data rows
93
+ if (block.includes("### Step 5:")) {
94
+ const step5Section = extractSection(block, "### Step 5:", "### Step 6:");
95
+ const dataRows = step5Section.split("\n").filter((l) => l.startsWith("|") && !l.includes("---") && !l.includes("No |"));
96
+ if (dataRows.length > 0) {
97
+ stepsDone.push(5);
98
+ }
99
+ else {
100
+ stepsPending.push(5);
101
+ }
102
+ }
103
+ // Step 6: check if execution table has data
104
+ if (block.includes("### Step 6:")) {
105
+ const step6Section = extractSection(block, "### Step 6:", "### Step 7:");
106
+ const dataRows = step6Section.split("\n").filter((l) => l.startsWith("|") && !l.includes("---") && !l.includes("No |"));
107
+ if (dataRows.length > 0) {
108
+ stepsDone.push(6);
109
+ }
110
+ else {
111
+ stepsPending.push(6);
112
+ }
113
+ }
114
+ // Step 7: check if evaluation is filled
115
+ if (block.includes("### Step 7:")) {
116
+ const step7Section = extractSection(block, "### Step 7:", "### Step 8:");
117
+ const dashCount = (step7Section.match(/: —/g) ?? []).length;
118
+ if (dashCount === 0) {
119
+ stepsDone.push(7);
120
+ }
121
+ else {
122
+ stepsPending.push(7);
123
+ }
124
+ }
125
+ // Step 8: check if validation is filled
126
+ if (block.includes("### Step 8:")) {
127
+ const step8Section = extractSection(block, "### Step 8:", null);
128
+ const dashCount = (step8Section.match(/: —/g) ?? []).length;
129
+ if (dashCount === 0) {
130
+ stepsDone.push(8);
131
+ }
132
+ else {
133
+ stepsPending.push(8);
134
+ }
135
+ }
136
+ // Current step = first pending
137
+ const currentStep = stepsPending.length > 0 ? stepsPending[0] : 8;
138
+ cycles.push({
139
+ qcc_id: qccId,
140
+ theme,
141
+ status,
142
+ created,
143
+ focus_target: focusTarget,
144
+ steps_done: stepsDone,
145
+ steps_pending: stepsPending,
146
+ current_step: currentStep,
147
+ });
148
+ }
149
+ return {
150
+ slug,
151
+ total_cycles: cycles.length,
152
+ active: cycles.filter((c) => c.status === "active").length,
153
+ completed: cycles.filter((c) => c.status === "completed").length,
154
+ cycles,
155
+ };
156
+ }
157
+ function extractSection(block, startHeader, endHeader) {
158
+ const startIdx = block.indexOf(startHeader);
159
+ if (startIdx === -1)
160
+ return "";
161
+ const afterHeader = startIdx + startHeader.length;
162
+ const endIdx = endHeader ? block.indexOf(endHeader, afterHeader) : block.length;
163
+ return endIdx === -1 ? block.slice(afterHeader) : block.slice(afterHeader, endIdx);
164
+ }
@@ -0,0 +1,188 @@
1
+ import { readFile, writeFile } from "fs/promises";
2
+ import { join } from "path";
3
+ export async function qccUpdate(claudeHome, args) {
4
+ const qccPath = join(claudeHome, "project-docs", args.slug, "QCC_CYCLES.md");
5
+ let content;
6
+ try {
7
+ content = await readFile(qccPath, "utf-8");
8
+ }
9
+ catch {
10
+ throw new Error(`QCC_CYCLES.md not found for project ${args.slug}. Run qcc_create first.`);
11
+ }
12
+ // Find the cycle section
13
+ const cycleHeader = `## [${args.qcc_id}]`;
14
+ const cycleIdx = content.indexOf(cycleHeader);
15
+ if (cycleIdx === -1) {
16
+ throw new Error(`Cycle ${args.qcc_id} not found in QCC_CYCLES.md`);
17
+ }
18
+ // Find the end of this cycle (next ## [ or end of file)
19
+ const nextCycleMatch = content.slice(cycleIdx + cycleHeader.length).match(/\n## \[QCC-/);
20
+ const cycleEnd = nextCycleMatch
21
+ ? cycleIdx + cycleHeader.length + nextCycleMatch.index
22
+ : content.length;
23
+ let cycleContent = content.slice(cycleIdx, cycleEnd);
24
+ const { step } = args.data;
25
+ const stepHeaders = {
26
+ 2: "### Step 2: Kondisi Saat Ini",
27
+ 3: "### Step 3: Target SMART",
28
+ 4: "### Step 4: Fishbone (Sebab-Akibat)",
29
+ 5: "### Step 5: Rencana Penanggulangan",
30
+ 6: "### Step 6: Pelaksanaan",
31
+ 7: "### Step 7: Evaluasi",
32
+ 8: "### Step 8: Validasi",
33
+ };
34
+ const currentHeader = stepHeaders[step];
35
+ const nextStep = step + 1;
36
+ const nextHeader = stepHeaders[nextStep] ?? null;
37
+ const headerIdx = cycleContent.indexOf(currentHeader);
38
+ if (headerIdx === -1) {
39
+ throw new Error(`Step ${step} header not found in cycle ${args.qcc_id}`);
40
+ }
41
+ const sectionStart = headerIdx + currentHeader.length;
42
+ const sectionEnd = nextHeader
43
+ ? cycleContent.indexOf(nextHeader)
44
+ : cycleContent.length;
45
+ if (sectionEnd === -1) {
46
+ throw new Error(`Cannot find boundary for step ${step}`);
47
+ }
48
+ let newSection;
49
+ switch (args.data.step) {
50
+ case 2: {
51
+ const lines = args.data.findings.map((f) => `- ${f}`);
52
+ newSection = "\n" + lines.join("\n") + "\n\n";
53
+ break;
54
+ }
55
+ case 3: {
56
+ const s = args.data.smart;
57
+ newSection = [
58
+ "",
59
+ `- **S (Specific/Masalah)**: ${s.specific}`,
60
+ `- **M (Measurable/Target)**: ${s.measurable}`,
61
+ `- **A (Achievable/Percepat)**: ${s.achievable}`,
62
+ `- **R (Relevant/Alasan)**: ${s.relevant}`,
63
+ `- **T (Time-bound/Waktu)**: ${s.timebound}`,
64
+ "",
65
+ ].join("\n");
66
+ break;
67
+ }
68
+ case 4: {
69
+ const { root_problem, causes } = args.data;
70
+ const mermaidLines = [
71
+ "```mermaid",
72
+ "graph LR",
73
+ ` ROOT["${root_problem}"]`,
74
+ ];
75
+ const catMap = {
76
+ man: "MAN",
77
+ machine: "MACHINE",
78
+ method: "METHOD",
79
+ material: "MATERIAL",
80
+ environment: "ENV",
81
+ };
82
+ const catLabel = {
83
+ man: "Man",
84
+ machine: "Machine",
85
+ method: "Method",
86
+ material: "Material",
87
+ environment: "Environment",
88
+ };
89
+ for (const cause of causes) {
90
+ const nodeId = catMap[cause.category] ?? cause.category.toUpperCase();
91
+ const label = catLabel[cause.category] ?? cause.category;
92
+ mermaidLines.push(` ROOT --- ${nodeId}["${label}"]`);
93
+ cause.items.forEach((item, i) => {
94
+ const itemId = `${nodeId}${i + 1}`;
95
+ mermaidLines.push(` ${nodeId} --- ${itemId}["${item}"]`);
96
+ });
97
+ }
98
+ mermaidLines.push("```");
99
+ newSection = "\n" + mermaidLines.join("\n") + "\n\n";
100
+ break;
101
+ }
102
+ case 5: {
103
+ const tableHeader = [
104
+ "| No | Sebab | Tindakan | PIC | Target |",
105
+ "|----|-------|----------|-----|--------|",
106
+ ];
107
+ const rows = args.data.countermeasures.map((cm, i) => `| ${i + 1} | ${cm.cause} | ${cm.action} | ${cm.pic} | ${cm.target_date} |`);
108
+ newSection = "\n" + [...tableHeader, ...rows].join("\n") + "\n\n";
109
+ break;
110
+ }
111
+ case 6: {
112
+ // For step 6, we UPDATE existing table rows or add new ones
113
+ const ex = args.data.execution;
114
+ const existingSection = cycleContent.slice(sectionStart, sectionEnd);
115
+ const tableLines = existingSection.split("\n").filter((l) => l.startsWith("|"));
116
+ if (tableLines.length < 2) {
117
+ // No table yet, create with header
118
+ newSection = [
119
+ "",
120
+ "| No | Tindakan | Status | Tanggal | Catatan |",
121
+ "|----|----------|--------|---------|---------|",
122
+ `| ${ex.no} | — | ${ex.status} | ${ex.date ?? "—"} | ${ex.notes ?? "—"} |`,
123
+ "",
124
+ ].join("\n");
125
+ }
126
+ else {
127
+ // Find existing row or append
128
+ const headerRows = tableLines.slice(0, 2);
129
+ const dataRows = tableLines.slice(2);
130
+ let found = false;
131
+ const updatedRows = dataRows.map((row) => {
132
+ const cols = row.split("|").map((c) => c.trim()).filter(Boolean);
133
+ if (cols[0] === String(ex.no)) {
134
+ found = true;
135
+ return `| ${ex.no} | ${cols[1]} | ${ex.status} | ${ex.date ?? cols[3]} | ${ex.notes ?? cols[4]} |`;
136
+ }
137
+ return row;
138
+ });
139
+ if (!found) {
140
+ updatedRows.push(`| ${ex.no} | — | ${ex.status} | ${ex.date ?? "—"} | ${ex.notes ?? "—"} |`);
141
+ }
142
+ newSection = "\n" + [...headerRows, ...updatedRows].join("\n") + "\n\n";
143
+ }
144
+ break;
145
+ }
146
+ case 7: {
147
+ const ev = args.data.evaluation;
148
+ newSection = [
149
+ "",
150
+ `- **Before**: ${ev.before}`,
151
+ `- **After**: ${ev.after}`,
152
+ `- **Target tercapai?**: ${ev.target_achieved ? "Ya" : "Belum"}`,
153
+ ev.notes ? `- **Notes**: ${ev.notes}` : null,
154
+ "",
155
+ ].filter(Boolean).join("\n");
156
+ break;
157
+ }
158
+ case 8: {
159
+ const v = args.data.validation;
160
+ const today = new Date().toISOString().slice(0, 10);
161
+ // Also update cycle status to completed
162
+ cycleContent = cycleContent.replace(/- \*\*Status\*\*: active/, "- **Status**: completed");
163
+ newSection = [
164
+ "",
165
+ `- **Validated by**: ${v.validated_by}`,
166
+ `- **Date**: ${v.date ?? today}`,
167
+ `- **Standardisasi**: ${v.standardization ?? "—"}`,
168
+ `- **Horizontal deploy**: ${v.horizontal_deploy ?? "—"}`,
169
+ "",
170
+ ].join("\n");
171
+ break;
172
+ }
173
+ }
174
+ // Replace section content
175
+ cycleContent =
176
+ cycleContent.slice(0, sectionStart) +
177
+ newSection +
178
+ cycleContent.slice(sectionEnd);
179
+ // Rebuild full content
180
+ content = content.slice(0, cycleIdx) + cycleContent + content.slice(cycleEnd);
181
+ await writeFile(qccPath, content, "utf-8");
182
+ return {
183
+ qcc_id: args.qcc_id,
184
+ step,
185
+ file: qccPath,
186
+ message: `Step ${step} updated for ${args.qcc_id}`,
187
+ };
188
+ }