@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.
- package/bin/cli.js +490 -0
- package/dist/index.js +454 -0
- package/dist/memory/memory-manager.js +234 -0
- package/dist/server/web-server.js +574 -0
- package/dist/tools/aggregate-patterns.js +101 -0
- package/dist/tools/analyze-history.js +213 -0
- package/dist/tools/auto-dispatch.js +199 -0
- package/dist/tools/check-energy.js +49 -0
- package/dist/tools/cross-search.js +171 -0
- package/dist/tools/get-focus.js +7 -0
- package/dist/tools/get-identity.js +7 -0
- package/dist/tools/get-project-status.js +35 -0
- package/dist/tools/list-projects.js +21 -0
- package/dist/tools/list-recent-tasks.js +59 -0
- package/dist/tools/log-insight.js +43 -0
- package/dist/tools/qcc-create.js +82 -0
- package/dist/tools/qcc-status.js +164 -0
- package/dist/tools/qcc-update.js +188 -0
- package/dist/tools/smart-bootstrap.js +255 -0
- package/dist/tools/summarize-session.js +161 -0
- package/dist/tools/switch-focus.js +40 -0
- package/dist/tools/workflow-router.js +438 -0
- package/package.json +44 -0
- package/templates/index.ts.template +42 -0
- package/templates/shared/get-claude-md.ts +12 -0
- package/templates/shared/get-current-state.ts +21 -0
- package/templates/shared/get-mistakes.ts +18 -0
- package/templates/shared/log-task.ts +27 -0
- package/templates/shared/predict-impact.ts +67 -0
- package/templates/shared/record-mistake.ts +40 -0
- package/templates/shared/update-state.ts +83 -0
- package/templates/stacks/express/config.json +9 -0
- package/templates/stacks/express/list-routes.ts +56 -0
- package/templates/stacks/express/symbol-index.ts +70 -0
- package/templates/stacks/laravel/config.json +9 -0
- package/templates/stacks/laravel/list-routes.ts +19 -0
- package/templates/stacks/laravel/symbol-index.ts +64 -0
- package/templates/stacks/nextjs/config.json +9 -0
- package/templates/stacks/nextjs/list-routes.ts +67 -0
- package/templates/stacks/nextjs/symbol-index.ts +78 -0
- package/templates/stacks/react/config.json +10 -0
- package/templates/stacks/react/list-routes.ts +44 -0
- 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
|
+
}
|