create-claude-kanban 3.2.2 → 4.0.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/dist/index.js +328 -52
- package/package.json +1 -1
- package/templates/hooks/auto-archive.sh +20 -0
- package/templates/kanban.cjs +79 -0
- package/templates/kanban.html +264 -1
- package/templates/orchestrator.md +26 -0
- package/templates/skills/archive.md +26 -0
- package/templates/skills/commit.md +35 -0
- package/templates/skills/deploy.md +49 -0
- package/templates/skills/standup.md +38 -0
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import * as
|
|
4
|
+
import * as p10 from "@clack/prompts";
|
|
5
5
|
import pc from "picocolors";
|
|
6
6
|
|
|
7
7
|
// src/prompts/project-info.ts
|
|
@@ -12,6 +12,9 @@ var lang = "en";
|
|
|
12
12
|
function setLang(l) {
|
|
13
13
|
lang = l;
|
|
14
14
|
}
|
|
15
|
+
function getLang() {
|
|
16
|
+
return lang;
|
|
17
|
+
}
|
|
15
18
|
function t(key, vars) {
|
|
16
19
|
let msg = messages[lang]?.[key] ?? key;
|
|
17
20
|
if (vars) {
|
|
@@ -311,6 +314,26 @@ var messages = {
|
|
|
311
314
|
"agentGen.reportContentDesc": "\uB0B4\uC6A9: \uBCC0\uACBD \uD30C\uC77C \uBAA9\uB85D, \uBCC0\uACBD \uB0B4\uC6A9, \uAC80\uC99D \uACB0\uACFC, \uD6C4\uC18D \uC791\uC5C5",
|
|
312
315
|
"agentGen.fontsLabel": "\uD3F0\uD2B8",
|
|
313
316
|
"agentGen.colorsLabel": "\uC0C9\uC0C1",
|
|
317
|
+
// ─── prompts/operations.ts ───
|
|
318
|
+
"operations.phase": "Phase 8: \uC6B4\uC601 \uC790\uB3D9\uD654",
|
|
319
|
+
"operations.autoArchiveMsg": "\uC138\uC158 \uC885\uB8CC \uC2DC \uC790\uB3D9 \uC544\uCE74\uC774\uBE0C (Hooks)?",
|
|
320
|
+
"operations.installSkillsMsg": "\uAE30\uBCF8 \uC2A4\uD0AC \uC124\uCE58 (/archive, /standup, /commit, /deploy)?",
|
|
321
|
+
"operations.memorySeedMsg": "MEMORY.md \uC2DC\uB4DC \uD30C\uC77C \uC0DD\uC131?",
|
|
322
|
+
"operations.claudeMdRulesMsg": "CLAUDE.md\uC5D0 \uCE78\uBC18 \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uADDC\uCE59 \uCD94\uAC00?",
|
|
323
|
+
// ─── scaffold.ts (operations) ───
|
|
324
|
+
"scaffold.hooksCreating": "\uC790\uB3D9 \uC544\uCE74\uC774\uBE0C \uD6C5 \uC0DD\uC131 \uC911...",
|
|
325
|
+
"scaffold.hooksDone": "\uC790\uB3D9 \uC544\uCE74\uC774\uBE0C \uD6C5 \uC0DD\uC131 \uC644\uB8CC",
|
|
326
|
+
"scaffold.skillsCreating": "\uC2A4\uD0AC \uD30C\uC77C \uC0DD\uC131 \uC911...",
|
|
327
|
+
"scaffold.skillsDone": "\uC2A4\uD0AC {count}\uAC1C \uC0DD\uC131 \uC644\uB8CC",
|
|
328
|
+
"scaffold.memorySeedCreating": "MEMORY.md \uC2DC\uB4DC \uC0DD\uC131 \uC911...",
|
|
329
|
+
"scaffold.memorySeedDone": "MEMORY.md \uC2DC\uB4DC \uC0DD\uC131 \uC644\uB8CC",
|
|
330
|
+
"scaffold.claudeMdCreating": "CLAUDE.md \uADDC\uCE59 \uCD94\uAC00 \uC911...",
|
|
331
|
+
"scaffold.claudeMdDone": "CLAUDE.md \uADDC\uCE59 \uCD94\uAC00 \uC644\uB8CC",
|
|
332
|
+
// ─── index.ts (operations) ───
|
|
333
|
+
"index.fileHooks": "\uC790\uB3D9 \uC544\uCE74\uC774\uBE0C \uD6C5",
|
|
334
|
+
"index.fileSkills": "\uC2A4\uD0AC {count}\uAC1C (/archive, /standup, /commit, /deploy)",
|
|
335
|
+
"index.fileMemorySeed": "MEMORY.md \uC2DC\uB4DC",
|
|
336
|
+
"index.fileClaude": "CLAUDE.md \uCE78\uBC18 \uADDC\uCE59",
|
|
314
337
|
// ─── utils/detect-structure.ts ───
|
|
315
338
|
"detect.src": "\uC18C\uC2A4 \uCF54\uB4DC",
|
|
316
339
|
"detect.app": "\uC571 \uC18C\uC2A4 (Next.js/Remix)",
|
|
@@ -623,6 +646,26 @@ var messages = {
|
|
|
623
646
|
"agentGen.reportContentDesc": "Content: changed files, changes, verification results, follow-up tasks",
|
|
624
647
|
"agentGen.fontsLabel": "Fonts",
|
|
625
648
|
"agentGen.colorsLabel": "Colors",
|
|
649
|
+
// ─── prompts/operations.ts ───
|
|
650
|
+
"operations.phase": "Phase 8: Operations automation",
|
|
651
|
+
"operations.autoArchiveMsg": "Auto-archive on session end (Hooks)?",
|
|
652
|
+
"operations.installSkillsMsg": "Install default skills (/archive, /standup, /commit, /deploy)?",
|
|
653
|
+
"operations.memorySeedMsg": "Generate MEMORY.md seed file?",
|
|
654
|
+
"operations.claudeMdRulesMsg": "Add kanban workflow rules to CLAUDE.md?",
|
|
655
|
+
// ─── scaffold.ts (operations) ───
|
|
656
|
+
"scaffold.hooksCreating": "Creating auto-archive hook...",
|
|
657
|
+
"scaffold.hooksDone": "Auto-archive hook created",
|
|
658
|
+
"scaffold.skillsCreating": "Creating skill files...",
|
|
659
|
+
"scaffold.skillsDone": "{count} skills created",
|
|
660
|
+
"scaffold.memorySeedCreating": "Creating MEMORY.md seed...",
|
|
661
|
+
"scaffold.memorySeedDone": "MEMORY.md seed created",
|
|
662
|
+
"scaffold.claudeMdCreating": "Adding CLAUDE.md rules...",
|
|
663
|
+
"scaffold.claudeMdDone": "CLAUDE.md rules added",
|
|
664
|
+
// ─── index.ts (operations) ───
|
|
665
|
+
"index.fileHooks": "Auto-archive hook",
|
|
666
|
+
"index.fileSkills": "{count} skills (/archive, /standup, /commit, /deploy)",
|
|
667
|
+
"index.fileMemorySeed": "MEMORY.md seed",
|
|
668
|
+
"index.fileClaude": "CLAUDE.md kanban rules",
|
|
626
669
|
// ─── utils/detect-structure.ts ───
|
|
627
670
|
"detect.src": "Source code",
|
|
628
671
|
"detect.app": "App source (Next.js/Remix)",
|
|
@@ -731,7 +774,7 @@ function detectStructure(targetDir) {
|
|
|
731
774
|
return dirs;
|
|
732
775
|
}
|
|
733
776
|
function detectProjectSignals(targetDir) {
|
|
734
|
-
const exists = (
|
|
777
|
+
const exists = (p11) => fs.existsSync(path.join(targetDir, p11));
|
|
735
778
|
return {
|
|
736
779
|
hasTypeScript: exists("tsconfig.json"),
|
|
737
780
|
hasPackageJson: exists("package.json"),
|
|
@@ -1391,6 +1434,38 @@ async function promptInfra(projectName) {
|
|
|
1391
1434
|
};
|
|
1392
1435
|
}
|
|
1393
1436
|
|
|
1437
|
+
// src/prompts/operations.ts
|
|
1438
|
+
import * as p8 from "@clack/prompts";
|
|
1439
|
+
async function promptOperations() {
|
|
1440
|
+
p8.note("", t("operations.phase"));
|
|
1441
|
+
const autoArchive = await p8.confirm({
|
|
1442
|
+
message: t("operations.autoArchiveMsg"),
|
|
1443
|
+
initialValue: true
|
|
1444
|
+
});
|
|
1445
|
+
if (p8.isCancel(autoArchive)) return null;
|
|
1446
|
+
const installSkills = await p8.confirm({
|
|
1447
|
+
message: t("operations.installSkillsMsg"),
|
|
1448
|
+
initialValue: true
|
|
1449
|
+
});
|
|
1450
|
+
if (p8.isCancel(installSkills)) return null;
|
|
1451
|
+
const memorySeed = await p8.confirm({
|
|
1452
|
+
message: t("operations.memorySeedMsg"),
|
|
1453
|
+
initialValue: true
|
|
1454
|
+
});
|
|
1455
|
+
if (p8.isCancel(memorySeed)) return null;
|
|
1456
|
+
const claudeMdRules = await p8.confirm({
|
|
1457
|
+
message: t("operations.claudeMdRulesMsg"),
|
|
1458
|
+
initialValue: true
|
|
1459
|
+
});
|
|
1460
|
+
if (p8.isCancel(claudeMdRules)) return null;
|
|
1461
|
+
return {
|
|
1462
|
+
autoArchive,
|
|
1463
|
+
installSkills,
|
|
1464
|
+
memorySeed,
|
|
1465
|
+
claudeMdRules
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1394
1469
|
// src/types.ts
|
|
1395
1470
|
var OPUS_AGENTS = /* @__PURE__ */ new Set(["orchestrator", "strategist", "item-author"]);
|
|
1396
1471
|
var HAIKU_AGENTS = /* @__PURE__ */ new Set(["doc-writer"]);
|
|
@@ -1417,6 +1492,8 @@ async function runPrompts() {
|
|
|
1417
1492
|
if (!additionalProjects) return null;
|
|
1418
1493
|
const infra = await promptInfra(project.name);
|
|
1419
1494
|
if (!infra) return null;
|
|
1495
|
+
const operations = await promptOperations();
|
|
1496
|
+
if (!operations) return null;
|
|
1420
1497
|
return {
|
|
1421
1498
|
project,
|
|
1422
1499
|
techStack,
|
|
@@ -1424,14 +1501,15 @@ async function runPrompts() {
|
|
|
1424
1501
|
agents,
|
|
1425
1502
|
orchestrator,
|
|
1426
1503
|
additionalProjects,
|
|
1427
|
-
infra
|
|
1504
|
+
infra,
|
|
1505
|
+
operations
|
|
1428
1506
|
};
|
|
1429
1507
|
}
|
|
1430
1508
|
|
|
1431
1509
|
// src/scaffold.ts
|
|
1432
|
-
import
|
|
1433
|
-
import
|
|
1434
|
-
import * as
|
|
1510
|
+
import path9 from "path";
|
|
1511
|
+
import fs10 from "fs-extra";
|
|
1512
|
+
import * as p9 from "@clack/prompts";
|
|
1435
1513
|
import { fileURLToPath } from "url";
|
|
1436
1514
|
|
|
1437
1515
|
// src/generators/config.ts
|
|
@@ -1954,13 +2032,173 @@ function generateEnvFile(answers) {
|
|
|
1954
2032
|
return lines.join("\n");
|
|
1955
2033
|
}
|
|
1956
2034
|
|
|
1957
|
-
// src/
|
|
1958
|
-
import { execSync } from "child_process";
|
|
2035
|
+
// src/generators/hooks.ts
|
|
1959
2036
|
import path4 from "path";
|
|
1960
2037
|
import fs5 from "fs-extra";
|
|
2038
|
+
async function generateHooks(answers, targetDir, templatesDir) {
|
|
2039
|
+
const hooksDir = path4.join(targetDir, ".claude", "hooks");
|
|
2040
|
+
await fs5.ensureDir(hooksDir);
|
|
2041
|
+
const src = path4.join(templatesDir, "hooks", "auto-archive.sh");
|
|
2042
|
+
if (!fs5.existsSync(src)) return;
|
|
2043
|
+
let content = await fs5.readFile(src, "utf-8");
|
|
2044
|
+
content = content.replace(/\{\{port\}\}/g, String(answers.infra.port));
|
|
2045
|
+
const projectId = answers.additionalProjects.length > 0 ? answers.project.name.toLowerCase().replace(/\s+/g, "-") : "kanban";
|
|
2046
|
+
content = content.replace(/\{\{projectId\}\}/g, projectId);
|
|
2047
|
+
const dest = path4.join(hooksDir, "auto-archive.sh");
|
|
2048
|
+
await fs5.writeFile(dest, content);
|
|
2049
|
+
await fs5.chmod(dest, 493);
|
|
2050
|
+
const settingsPath = path4.join(targetDir, ".claude", "settings.json");
|
|
2051
|
+
let settings = {};
|
|
2052
|
+
if (fs5.existsSync(settingsPath)) {
|
|
2053
|
+
try {
|
|
2054
|
+
settings = JSON.parse(await fs5.readFile(settingsPath, "utf-8"));
|
|
2055
|
+
} catch {
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
const hooks = settings.hooks || {};
|
|
2059
|
+
const sessionEnd = hooks.SessionEnd || [];
|
|
2060
|
+
const hookCommand = path4.join(".claude", "hooks", "auto-archive.sh");
|
|
2061
|
+
const alreadyExists = sessionEnd.some((h) => h.command === hookCommand);
|
|
2062
|
+
if (!alreadyExists) {
|
|
2063
|
+
sessionEnd.push({ command: hookCommand });
|
|
2064
|
+
hooks.SessionEnd = sessionEnd;
|
|
2065
|
+
settings.hooks = hooks;
|
|
2066
|
+
}
|
|
2067
|
+
await fs5.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// src/generators/skills.ts
|
|
2071
|
+
import path5 from "path";
|
|
2072
|
+
import fs6 from "fs-extra";
|
|
2073
|
+
var SKILL_NAMES = ["archive", "standup", "commit", "deploy"];
|
|
2074
|
+
async function generateSkills(answers, targetDir, templatesDir) {
|
|
2075
|
+
const projectId = answers.additionalProjects.length > 0 ? answers.project.name.toLowerCase().replace(/\s+/g, "-") : "kanban";
|
|
2076
|
+
let count = 0;
|
|
2077
|
+
for (const name of SKILL_NAMES) {
|
|
2078
|
+
const src = path5.join(templatesDir, "skills", `${name}.md`);
|
|
2079
|
+
if (!fs6.existsSync(src)) continue;
|
|
2080
|
+
let content = await fs6.readFile(src, "utf-8");
|
|
2081
|
+
content = content.replace(/\{\{port\}\}/g, String(answers.infra.port));
|
|
2082
|
+
content = content.replace(/\{\{projectId\}\}/g, projectId);
|
|
2083
|
+
content = content.replace(/\{\{projectName\}\}/g, answers.project.name);
|
|
2084
|
+
const skillDir = path5.join(targetDir, ".claude", "skills", name);
|
|
2085
|
+
await fs6.ensureDir(skillDir);
|
|
2086
|
+
await fs6.writeFile(path5.join(skillDir, "SKILL.md"), content);
|
|
2087
|
+
count++;
|
|
2088
|
+
}
|
|
2089
|
+
return count;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// src/generators/memory-seed.ts
|
|
2093
|
+
import path6 from "path";
|
|
2094
|
+
import fs7 from "fs-extra";
|
|
2095
|
+
async function generateMemorySeed(answers, targetDir) {
|
|
2096
|
+
const lang2 = getLang();
|
|
2097
|
+
const agents = answers.agents.selectedAgents.join(", ");
|
|
2098
|
+
const stack = [
|
|
2099
|
+
answers.techStack.frontend,
|
|
2100
|
+
...answers.techStack.backend,
|
|
2101
|
+
answers.techStack.deploy
|
|
2102
|
+
].filter(Boolean).join(", ");
|
|
2103
|
+
const conventions = answers.contextFiles.codingStyle.length > 0 ? answers.contextFiles.codingStyle.join(", ") : "";
|
|
2104
|
+
const lines = [];
|
|
2105
|
+
if (lang2 === "ko") {
|
|
2106
|
+
lines.push(`# ${answers.project.name} \uD504\uB85C\uC81D\uD2B8 \uBA54\uBAA8\uB9AC`);
|
|
2107
|
+
lines.push("");
|
|
2108
|
+
lines.push("## \uD504\uB85C\uC81D\uD2B8");
|
|
2109
|
+
lines.push(`- \uC774\uB984: ${answers.project.name}`);
|
|
2110
|
+
lines.push(`- \uC124\uBA85: ${answers.project.description}`);
|
|
2111
|
+
lines.push(`- \uC2A4\uD0DD: ${stack || "(\uBBF8\uC815)"}`);
|
|
2112
|
+
lines.push(`- \uB514\uB809\uD1A0\uB9AC: ${targetDir}`);
|
|
2113
|
+
lines.push(`- \uCE78\uBC18: http://localhost:${answers.infra.port}`);
|
|
2114
|
+
lines.push("");
|
|
2115
|
+
lines.push("## \uC5D0\uC774\uC804\uD2B8");
|
|
2116
|
+
lines.push(`- \uAD6C\uC131: ${agents}`);
|
|
2117
|
+
lines.push(`- \uAE30\uBCF8 \uBAA8\uB378: ${answers.agents.defaultModel}`);
|
|
2118
|
+
if (conventions) {
|
|
2119
|
+
lines.push("");
|
|
2120
|
+
lines.push("## \uCEE8\uBCA4\uC158");
|
|
2121
|
+
lines.push(`- ${conventions}`);
|
|
2122
|
+
}
|
|
2123
|
+
lines.push("");
|
|
2124
|
+
lines.push("## \uBA54\uBAA8");
|
|
2125
|
+
lines.push("<!-- \uC5EC\uAE30\uC5D0 \uD504\uB85C\uC81D\uD2B8 \uC9C4\uD589 \uC911 \uBC30\uC6B4 \uB0B4\uC6A9\uC744 \uAE30\uB85D\uD558\uC138\uC694 -->");
|
|
2126
|
+
} else {
|
|
2127
|
+
lines.push(`# ${answers.project.name} Project Memory`);
|
|
2128
|
+
lines.push("");
|
|
2129
|
+
lines.push("## Project");
|
|
2130
|
+
lines.push(`- Name: ${answers.project.name}`);
|
|
2131
|
+
lines.push(`- Description: ${answers.project.description}`);
|
|
2132
|
+
lines.push(`- Stack: ${stack || "(undecided)"}`);
|
|
2133
|
+
lines.push(`- Directory: ${targetDir}`);
|
|
2134
|
+
lines.push(`- Kanban: http://localhost:${answers.infra.port}`);
|
|
2135
|
+
lines.push("");
|
|
2136
|
+
lines.push("## Agents");
|
|
2137
|
+
lines.push(`- Team: ${agents}`);
|
|
2138
|
+
lines.push(`- Default model: ${answers.agents.defaultModel}`);
|
|
2139
|
+
if (conventions) {
|
|
2140
|
+
lines.push("");
|
|
2141
|
+
lines.push("## Conventions");
|
|
2142
|
+
lines.push(`- ${conventions}`);
|
|
2143
|
+
}
|
|
2144
|
+
lines.push("");
|
|
2145
|
+
lines.push("## Notes");
|
|
2146
|
+
lines.push("<!-- Record learnings from the project here -->");
|
|
2147
|
+
}
|
|
2148
|
+
await fs7.writeFile(
|
|
2149
|
+
path6.join(targetDir, ".claude", "memory-seed.md"),
|
|
2150
|
+
lines.join("\n") + "\n"
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// src/generators/claude-md.ts
|
|
2155
|
+
import path7 from "path";
|
|
2156
|
+
import fs8 from "fs-extra";
|
|
2157
|
+
var MARKER = "<!-- kanban-workflow-rules -->";
|
|
2158
|
+
function getRules(lang2, port) {
|
|
2159
|
+
if (lang2 === "ko") {
|
|
2160
|
+
return [
|
|
2161
|
+
MARKER,
|
|
2162
|
+
"## \uCE78\uBC18 \uC6CC\uD06C\uD50C\uB85C\uC6B0",
|
|
2163
|
+
`- \uCE78\uBC18 \uBCF4\uB4DC: http://localhost:${port} (node tools/kanban.cjs)`,
|
|
2164
|
+
"- \uD0DC\uC2A4\uD06C \uC2DC\uC791 \uC2DC status\uB97C in_progress\uB85C \uBCC0\uACBD\uD558\uACE0, \uC644\uB8CC \uC2DC completed\uB85C \uBCC0\uACBD",
|
|
2165
|
+
"- \uC644\uB8CC\uB41C \uD0DC\uC2A4\uD06C\uB294 /archive \uB610\uB294 Archive Done \uBC84\uD2BC\uC73C\uB85C \uC544\uCE74\uC774\uBE0C",
|
|
2166
|
+
"- \uB9E4\uC77C /standup\uC73C\uB85C \uC9C4\uD589 \uC0C1\uD669 \uD655\uC778",
|
|
2167
|
+
"- 3\uD68C \uC774\uC0C1 \uBC18\uBCF5 \uC791\uC5C5\uC740 \uC2A4\uD0AC(.claude/skills/)\uB85C \uC804\uD658 \uAC80\uD1A0"
|
|
2168
|
+
].join("\n");
|
|
2169
|
+
}
|
|
2170
|
+
return [
|
|
2171
|
+
MARKER,
|
|
2172
|
+
"## Kanban Workflow",
|
|
2173
|
+
`- Kanban board: http://localhost:${port} (node tools/kanban.cjs)`,
|
|
2174
|
+
"- Set task status to in_progress on start, completed on finish",
|
|
2175
|
+
"- Archive completed tasks via /archive or Archive Done button",
|
|
2176
|
+
"- Run /standup daily to review progress",
|
|
2177
|
+
"- Consider converting tasks repeated 3+ times into skills (.claude/skills/)"
|
|
2178
|
+
].join("\n");
|
|
2179
|
+
}
|
|
2180
|
+
async function generateClaudeMdRules(answers, targetDir) {
|
|
2181
|
+
const lang2 = getLang();
|
|
2182
|
+
const rules = getRules(lang2, answers.infra.port);
|
|
2183
|
+
const claudeMdPath = path7.join(targetDir, "CLAUDE.md");
|
|
2184
|
+
if (fs8.existsSync(claudeMdPath)) {
|
|
2185
|
+
const existing = await fs8.readFile(claudeMdPath, "utf-8");
|
|
2186
|
+
if (existing.includes(MARKER)) return;
|
|
2187
|
+
await fs8.writeFile(claudeMdPath, existing.trimEnd() + "\n\n" + rules + "\n");
|
|
2188
|
+
} else {
|
|
2189
|
+
await fs8.writeFile(claudeMdPath, `# ${answers.project.name}
|
|
2190
|
+
|
|
2191
|
+
` + rules + "\n");
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// src/utils/git.ts
|
|
2196
|
+
import { execSync } from "child_process";
|
|
2197
|
+
import path8 from "path";
|
|
2198
|
+
import fs9 from "fs-extra";
|
|
1961
2199
|
async function initGit(targetDir) {
|
|
1962
|
-
const gitDir =
|
|
1963
|
-
if (
|
|
2200
|
+
const gitDir = path8.join(targetDir, ".git");
|
|
2201
|
+
if (fs9.existsSync(gitDir)) return;
|
|
1964
2202
|
try {
|
|
1965
2203
|
execSync("git init", { cwd: targetDir, stdio: "ignore" });
|
|
1966
2204
|
} catch {
|
|
@@ -1969,43 +2207,49 @@ async function initGit(targetDir) {
|
|
|
1969
2207
|
|
|
1970
2208
|
// src/scaffold.ts
|
|
1971
2209
|
var __filename = fileURLToPath(import.meta.url);
|
|
1972
|
-
var __dirname =
|
|
2210
|
+
var __dirname = path9.dirname(__filename);
|
|
1973
2211
|
function getTemplatesDir() {
|
|
1974
|
-
const devPath =
|
|
1975
|
-
if (
|
|
1976
|
-
return
|
|
2212
|
+
const devPath = path9.join(__dirname, "..", "templates");
|
|
2213
|
+
if (fs10.existsSync(devPath)) return devPath;
|
|
2214
|
+
return path9.join(__dirname, "..", "templates");
|
|
1977
2215
|
}
|
|
1978
2216
|
async function scaffold(answers) {
|
|
1979
|
-
const targetDir =
|
|
2217
|
+
const targetDir = path9.resolve(answers.infra.targetDir);
|
|
1980
2218
|
const templatesDir = getTemplatesDir();
|
|
1981
|
-
const s =
|
|
2219
|
+
const s = p9.spinner();
|
|
1982
2220
|
s.start(t("scaffold.dirCreating"));
|
|
1983
|
-
await
|
|
1984
|
-
await
|
|
1985
|
-
await
|
|
1986
|
-
|
|
2221
|
+
await fs10.ensureDir(path9.join(targetDir, "tools"));
|
|
2222
|
+
await fs10.ensureDir(path9.join(targetDir, ".claude", "agents"));
|
|
2223
|
+
await fs10.ensureDir(path9.join(targetDir, ".claude", "logs"));
|
|
2224
|
+
if (answers.operations.installSkills) {
|
|
2225
|
+
await fs10.ensureDir(path9.join(targetDir, ".claude", "skills"));
|
|
2226
|
+
}
|
|
2227
|
+
if (answers.operations.autoArchive) {
|
|
2228
|
+
await fs10.ensureDir(path9.join(targetDir, ".claude", "hooks"));
|
|
2229
|
+
}
|
|
2230
|
+
await fs10.ensureDir(path9.join(targetDir, ".claude", "tasks", "kanban"));
|
|
1987
2231
|
for (const proj of answers.additionalProjects) {
|
|
1988
|
-
await
|
|
2232
|
+
await fs10.ensureDir(path9.join(targetDir, ".claude", "tasks", proj.id));
|
|
1989
2233
|
}
|
|
1990
2234
|
s.stop(t("scaffold.dirDone"));
|
|
1991
2235
|
s.start(t("scaffold.kanbanServerCopying"));
|
|
1992
|
-
const kanbanSrc =
|
|
1993
|
-
const kanbanDest =
|
|
1994
|
-
if (
|
|
1995
|
-
await
|
|
2236
|
+
const kanbanSrc = path9.join(templatesDir, "kanban.cjs");
|
|
2237
|
+
const kanbanDest = path9.join(targetDir, "tools", "kanban.cjs");
|
|
2238
|
+
if (fs10.existsSync(kanbanSrc)) {
|
|
2239
|
+
await fs10.copy(kanbanSrc, kanbanDest);
|
|
1996
2240
|
}
|
|
1997
2241
|
s.stop(t("scaffold.kanbanServerDone"));
|
|
1998
2242
|
s.start(t("scaffold.kanbanUICopying"));
|
|
1999
|
-
const htmlSrc =
|
|
2000
|
-
const htmlDest =
|
|
2001
|
-
if (
|
|
2002
|
-
await
|
|
2243
|
+
const htmlSrc = path9.join(templatesDir, "kanban.html");
|
|
2244
|
+
const htmlDest = path9.join(targetDir, "tools", "kanban.html");
|
|
2245
|
+
if (fs10.existsSync(htmlSrc)) {
|
|
2246
|
+
await fs10.copy(htmlSrc, htmlDest);
|
|
2003
2247
|
}
|
|
2004
2248
|
s.stop(t("scaffold.kanbanUIDone"));
|
|
2005
2249
|
s.start(t("scaffold.configCreating"));
|
|
2006
2250
|
const config = generateConfig(answers);
|
|
2007
|
-
await
|
|
2008
|
-
|
|
2251
|
+
await fs10.writeFile(
|
|
2252
|
+
path9.join(targetDir, "kanban.config.json"),
|
|
2009
2253
|
JSON.stringify(config, null, 2)
|
|
2010
2254
|
);
|
|
2011
2255
|
s.stop(t("scaffold.configDone"));
|
|
@@ -2014,15 +2258,15 @@ async function scaffold(answers) {
|
|
|
2014
2258
|
...proj,
|
|
2015
2259
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2016
2260
|
}));
|
|
2017
|
-
await
|
|
2018
|
-
|
|
2261
|
+
await fs10.writeFile(
|
|
2262
|
+
path9.join(targetDir, ".claude", "tasks", "_projects.json"),
|
|
2019
2263
|
JSON.stringify(projects, null, 2)
|
|
2020
2264
|
);
|
|
2021
2265
|
s.stop(t("scaffold.projectListDone"));
|
|
2022
2266
|
s.start(t("scaffold.contextCreating"));
|
|
2023
2267
|
const projectContext = generateProjectContext(answers);
|
|
2024
|
-
await
|
|
2025
|
-
|
|
2268
|
+
await fs10.writeFile(
|
|
2269
|
+
path9.join(targetDir, ".claude", "agents", "_project-context.md"),
|
|
2026
2270
|
projectContext
|
|
2027
2271
|
);
|
|
2028
2272
|
s.stop(t("scaffold.contextDone"));
|
|
@@ -2030,16 +2274,16 @@ async function scaffold(answers) {
|
|
|
2030
2274
|
for (const agent of answers.agents.selectedAgents) {
|
|
2031
2275
|
const model = getModelForAgent(agent, answers.agents.defaultModel, answers.agents.modelOverrides);
|
|
2032
2276
|
const prompt = generateAgentPrompt(agent, answers, model, templatesDir);
|
|
2033
|
-
await
|
|
2034
|
-
|
|
2277
|
+
await fs10.writeFile(
|
|
2278
|
+
path9.join(targetDir, ".claude", "agents", `${agent}.md`),
|
|
2035
2279
|
prompt
|
|
2036
2280
|
);
|
|
2037
2281
|
}
|
|
2038
2282
|
s.stop(t("scaffold.agentsDone", { count: answers.agents.selectedAgents.length }));
|
|
2039
2283
|
s.start(t("scaffold.orchestratorCreating"));
|
|
2040
|
-
const orchSrc =
|
|
2041
|
-
if (
|
|
2042
|
-
let orchContent = await
|
|
2284
|
+
const orchSrc = path9.join(templatesDir, "orchestrator.md");
|
|
2285
|
+
if (fs10.existsSync(orchSrc)) {
|
|
2286
|
+
let orchContent = await fs10.readFile(orchSrc, "utf-8");
|
|
2043
2287
|
orchContent = orchContent.replace(/\{\{projectName\}\}/g, answers.project.name);
|
|
2044
2288
|
orchContent = orchContent.replace(/\{\{port\}\}/g, String(answers.infra.port));
|
|
2045
2289
|
orchContent = orchContent.replace(/\{\{goals\}\}/g, answers.orchestrator.goals);
|
|
@@ -2050,41 +2294,61 @@ async function scaffold(answers) {
|
|
|
2050
2294
|
return `| ${a} | ${model} |`;
|
|
2051
2295
|
});
|
|
2052
2296
|
orchContent = orchContent.replace("{{agentTable}}", agentRows.join("\n"));
|
|
2053
|
-
await
|
|
2054
|
-
|
|
2297
|
+
await fs10.writeFile(
|
|
2298
|
+
path9.join(targetDir, ".claude", "orchestrator.md"),
|
|
2055
2299
|
orchContent
|
|
2056
2300
|
);
|
|
2057
2301
|
}
|
|
2058
2302
|
s.stop(t("scaffold.orchestratorDone"));
|
|
2059
2303
|
s.start(t("scaffold.envCreating"));
|
|
2060
2304
|
const envContent = generateEnvFile(answers);
|
|
2061
|
-
await
|
|
2062
|
-
|
|
2305
|
+
await fs10.writeFile(
|
|
2306
|
+
path9.join(targetDir, ".env.example"),
|
|
2063
2307
|
envContent
|
|
2064
2308
|
);
|
|
2065
2309
|
s.stop(t("scaffold.envDone"));
|
|
2066
2310
|
await initGit(targetDir);
|
|
2311
|
+
if (answers.operations.autoArchive) {
|
|
2312
|
+
s.start(t("scaffold.hooksCreating"));
|
|
2313
|
+
await generateHooks(answers, targetDir, templatesDir);
|
|
2314
|
+
s.stop(t("scaffold.hooksDone"));
|
|
2315
|
+
}
|
|
2316
|
+
if (answers.operations.installSkills) {
|
|
2317
|
+
s.start(t("scaffold.skillsCreating"));
|
|
2318
|
+
const skillCount = await generateSkills(answers, targetDir, templatesDir);
|
|
2319
|
+
s.stop(t("scaffold.skillsDone", { count: skillCount }));
|
|
2320
|
+
}
|
|
2321
|
+
if (answers.operations.memorySeed) {
|
|
2322
|
+
s.start(t("scaffold.memorySeedCreating"));
|
|
2323
|
+
await generateMemorySeed(answers, targetDir);
|
|
2324
|
+
s.stop(t("scaffold.memorySeedDone"));
|
|
2325
|
+
}
|
|
2326
|
+
if (answers.operations.claudeMdRules) {
|
|
2327
|
+
s.start(t("scaffold.claudeMdCreating"));
|
|
2328
|
+
await generateClaudeMdRules(answers, targetDir);
|
|
2329
|
+
s.stop(t("scaffold.claudeMdDone"));
|
|
2330
|
+
}
|
|
2067
2331
|
}
|
|
2068
2332
|
|
|
2069
2333
|
// src/index.ts
|
|
2070
2334
|
async function main() {
|
|
2071
2335
|
console.log();
|
|
2072
|
-
|
|
2073
|
-
const langChoice = await
|
|
2336
|
+
p10.intro(`${pc.bgCyan(pc.black(" create-claude-kanban "))} ${pc.dim("v4.0.0")}`);
|
|
2337
|
+
const langChoice = await p10.select({
|
|
2074
2338
|
message: "Language / \uC5B8\uC5B4",
|
|
2075
2339
|
options: [
|
|
2076
2340
|
{ value: "en", label: "English" },
|
|
2077
2341
|
{ value: "ko", label: "\uD55C\uAD6D\uC5B4" }
|
|
2078
2342
|
]
|
|
2079
2343
|
});
|
|
2080
|
-
if (
|
|
2081
|
-
|
|
2344
|
+
if (p10.isCancel(langChoice)) {
|
|
2345
|
+
p10.cancel("Cancelled.");
|
|
2082
2346
|
process.exit(0);
|
|
2083
2347
|
}
|
|
2084
2348
|
setLang(langChoice);
|
|
2085
2349
|
const answers = await runPrompts();
|
|
2086
2350
|
if (!answers) {
|
|
2087
|
-
|
|
2351
|
+
p10.cancel(t("common.cancelled"));
|
|
2088
2352
|
process.exit(0);
|
|
2089
2353
|
}
|
|
2090
2354
|
await scaffold(answers);
|
|
@@ -2098,11 +2362,23 @@ async function main() {
|
|
|
2098
2362
|
`.claude/orchestrator.md \u2014 ${t("index.fileOrchestrator")}`,
|
|
2099
2363
|
`.env.example \u2014 ${t("index.fileEnv")}`
|
|
2100
2364
|
];
|
|
2101
|
-
|
|
2102
|
-
|
|
2365
|
+
if (answers.operations.autoArchive) {
|
|
2366
|
+
files.push(`.claude/hooks/ \u2014 ${t("index.fileHooks")}`);
|
|
2367
|
+
}
|
|
2368
|
+
if (answers.operations.installSkills) {
|
|
2369
|
+
files.push(`.claude/skills/ \u2014 ${t("index.fileSkills", { count: 4 })}`);
|
|
2370
|
+
}
|
|
2371
|
+
if (answers.operations.memorySeed) {
|
|
2372
|
+
files.push(`.claude/memory-seed.md \u2014 ${t("index.fileMemorySeed")}`);
|
|
2373
|
+
}
|
|
2374
|
+
if (answers.operations.claudeMdRules) {
|
|
2375
|
+
files.push(`CLAUDE.md \u2014 ${t("index.fileClaude")}`);
|
|
2376
|
+
}
|
|
2377
|
+
p10.note(files.join("\n"), t("index.filesHeader"));
|
|
2378
|
+
p10.outro(`${pc.green("node tools/kanban.cjs")} ${t("index.startCmd")}`);
|
|
2103
2379
|
}
|
|
2104
2380
|
main().catch((err) => {
|
|
2105
|
-
|
|
2381
|
+
p10.cancel(t("common.error"));
|
|
2106
2382
|
console.error(err);
|
|
2107
2383
|
process.exit(1);
|
|
2108
2384
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Auto-archive completed tasks on session end
|
|
3
|
+
# Generated by create-claude-kanban
|
|
4
|
+
|
|
5
|
+
PORT={{port}}
|
|
6
|
+
PROJECT={{projectId}}
|
|
7
|
+
|
|
8
|
+
# Check if kanban server is running
|
|
9
|
+
if ! curl -s "http://localhost:${PORT}/api/tasks" > /dev/null 2>&1; then
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# Count completed tasks
|
|
14
|
+
COMPLETED=$(curl -s "http://localhost:${PORT}/api/tasks" | grep -o '"status":"completed"' | wc -l | tr -d ' ')
|
|
15
|
+
|
|
16
|
+
if [ "$COMPLETED" -gt "0" ]; then
|
|
17
|
+
curl -s -X POST "http://localhost:${PORT}/api/archive" \
|
|
18
|
+
-H "Content-Type: application/json" \
|
|
19
|
+
-d "{\"project\":\"${PROJECT}\"}" > /dev/null 2>&1
|
|
20
|
+
fi
|
package/templates/kanban.cjs
CHANGED
|
@@ -2091,6 +2091,84 @@ const server = http.createServer(async (req, res) => {
|
|
|
2091
2091
|
return;
|
|
2092
2092
|
}
|
|
2093
2093
|
|
|
2094
|
+
// GET /api/stats?project=<id>&period=week|month
|
|
2095
|
+
if (req.url.split("?")[0] === "/api/stats" && req.method === "GET") {
|
|
2096
|
+
var statsParams = new URL(req.url, "http://localhost").searchParams;
|
|
2097
|
+
var statsProjId = statsParams.get("project") || (readProjects()[0] || {}).id || "apex";
|
|
2098
|
+
var statsPeriod = statsParams.get("period") || "week";
|
|
2099
|
+
var statsProjects = readProjects();
|
|
2100
|
+
var statsProj = statsProjects.find(function(p) { return p.id === statsProjId; });
|
|
2101
|
+
var statsDirName = statsProj ? (statsProj.dir || statsProj.id) : statsProjId;
|
|
2102
|
+
|
|
2103
|
+
// Current board state
|
|
2104
|
+
var statsTasks = readAllTasks(statsProjId);
|
|
2105
|
+
var boardStatus = { pending: 0, in_progress: 0, in_review: 0, completed: 0 };
|
|
2106
|
+
statsTasks.forEach(function(t) {
|
|
2107
|
+
if (boardStatus.hasOwnProperty(t.status)) boardStatus[t.status]++;
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
// Aggregate from archives
|
|
2111
|
+
var statsArchDir = path.join(ARCHIVES_DIR, statsDirName);
|
|
2112
|
+
var daysBack = statsPeriod === "month" ? 30 : 7;
|
|
2113
|
+
var cutoff = new Date();
|
|
2114
|
+
cutoff.setDate(cutoff.getDate() - daysBack);
|
|
2115
|
+
var cutoffStr = cutoff.toISOString().slice(0, 10);
|
|
2116
|
+
|
|
2117
|
+
var totalCompleted = 0;
|
|
2118
|
+
var agentCounts = {};
|
|
2119
|
+
var dailyTrend = [];
|
|
2120
|
+
var totalDuration = 0;
|
|
2121
|
+
var durationCount = 0;
|
|
2122
|
+
|
|
2123
|
+
if (fs.existsSync(statsArchDir)) {
|
|
2124
|
+
try {
|
|
2125
|
+
var statsFiles = fs.readdirSync(statsArchDir).filter(function(f) { return f.endsWith(".json"); }).sort();
|
|
2126
|
+
statsFiles.forEach(function(f) {
|
|
2127
|
+
var dateStr = f.replace(".json", "");
|
|
2128
|
+
if (dateStr < cutoffStr) return;
|
|
2129
|
+
try {
|
|
2130
|
+
var data = JSON.parse(fs.readFileSync(path.join(statsArchDir, f), "utf-8"));
|
|
2131
|
+
var tasks = data.tasks || [];
|
|
2132
|
+
var dayCount = tasks.length;
|
|
2133
|
+
totalCompleted += dayCount;
|
|
2134
|
+
tasks.forEach(function(t) {
|
|
2135
|
+
var agent = t.agent || t.owner || "unknown";
|
|
2136
|
+
agentCounts[agent] = (agentCounts[agent] || 0) + 1;
|
|
2137
|
+
if (t.createdAt && t.completedAt) {
|
|
2138
|
+
var dur = new Date(t.completedAt).getTime() - new Date(t.createdAt).getTime();
|
|
2139
|
+
if (dur > 0) { totalDuration += dur; durationCount++; }
|
|
2140
|
+
}
|
|
2141
|
+
});
|
|
2142
|
+
dailyTrend.push({ date: dateStr, count: dayCount });
|
|
2143
|
+
} catch {}
|
|
2144
|
+
});
|
|
2145
|
+
} catch {}
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
// Also count current completed tasks
|
|
2149
|
+
var currentCompleted = statsTasks.filter(function(t) { return t.status === "completed"; });
|
|
2150
|
+
totalCompleted += currentCompleted.length;
|
|
2151
|
+
|
|
2152
|
+
var agentList = Object.keys(agentCounts).map(function(a) {
|
|
2153
|
+
return { agent: a, count: agentCounts[a] };
|
|
2154
|
+
}).sort(function(a, b) { return b.count - a.count; });
|
|
2155
|
+
|
|
2156
|
+
var avgDurationMs = durationCount > 0 ? Math.round(totalDuration / durationCount) : 0;
|
|
2157
|
+
var avgDurationMin = Math.round(avgDurationMs / 60000);
|
|
2158
|
+
|
|
2159
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2160
|
+
res.end(JSON.stringify({
|
|
2161
|
+
period: statsPeriod,
|
|
2162
|
+
project: statsProjId,
|
|
2163
|
+
board: boardStatus,
|
|
2164
|
+
totalCompleted: totalCompleted,
|
|
2165
|
+
avgDurationMin: avgDurationMin,
|
|
2166
|
+
agents: agentList,
|
|
2167
|
+
trend: dailyTrend,
|
|
2168
|
+
}));
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2094
2172
|
// DELETE /api/tasks/:id
|
|
2095
2173
|
const delMatch = req.url.match(/^\/api\/tasks\/([\w.-]+)$/);
|
|
2096
2174
|
if (delMatch && req.method === "DELETE") {
|
|
@@ -2575,6 +2653,7 @@ server.listen(PORT, () => {
|
|
|
2575
2653
|
console.log(" C : chat panel");
|
|
2576
2654
|
console.log(" A : activity panel");
|
|
2577
2655
|
console.log(" R : archive panel");
|
|
2656
|
+
console.log(" S : stats panel");
|
|
2578
2657
|
console.log(" Drag : move between columns");
|
|
2579
2658
|
console.log(" Ctrl+Shift+K : stop execution");
|
|
2580
2659
|
console.log(" Ctrl+C : quit");
|
package/templates/kanban.html
CHANGED
|
@@ -1478,6 +1478,175 @@
|
|
|
1478
1478
|
}
|
|
1479
1479
|
.review-btn.reject:hover { background: #EF444415; }
|
|
1480
1480
|
|
|
1481
|
+
/* ── Stats Panel ── */
|
|
1482
|
+
.stats-panel {
|
|
1483
|
+
position: fixed;
|
|
1484
|
+
top: 0;
|
|
1485
|
+
right: -480px;
|
|
1486
|
+
width: 480px;
|
|
1487
|
+
max-width: 100vw;
|
|
1488
|
+
height: 100vh;
|
|
1489
|
+
background: var(--s1);
|
|
1490
|
+
border-left: 1px solid var(--b1);
|
|
1491
|
+
z-index: 310;
|
|
1492
|
+
transition: right 0.2s ease;
|
|
1493
|
+
display: flex;
|
|
1494
|
+
flex-direction: column;
|
|
1495
|
+
}
|
|
1496
|
+
.stats-panel.open { right: 0; }
|
|
1497
|
+
.stats-panel .stats-header {
|
|
1498
|
+
padding: 14px 20px;
|
|
1499
|
+
border-bottom: 1px solid var(--b1);
|
|
1500
|
+
display: flex;
|
|
1501
|
+
align-items: center;
|
|
1502
|
+
justify-content: space-between;
|
|
1503
|
+
flex-shrink: 0;
|
|
1504
|
+
}
|
|
1505
|
+
.stats-panel .stats-header h3 {
|
|
1506
|
+
font-size: 13px;
|
|
1507
|
+
font-weight: 600;
|
|
1508
|
+
display: flex;
|
|
1509
|
+
align-items: center;
|
|
1510
|
+
gap: 8px;
|
|
1511
|
+
}
|
|
1512
|
+
.stats-panel .stats-close {
|
|
1513
|
+
background: none;
|
|
1514
|
+
border: 1px solid var(--b1);
|
|
1515
|
+
border-radius: var(--r);
|
|
1516
|
+
color: var(--t3);
|
|
1517
|
+
cursor: pointer;
|
|
1518
|
+
font-size: 14px;
|
|
1519
|
+
width: 28px; height: 28px;
|
|
1520
|
+
display: flex; align-items: center; justify-content: center;
|
|
1521
|
+
}
|
|
1522
|
+
.stats-panel .stats-close:hover { color: var(--t1); background: var(--s3); }
|
|
1523
|
+
.stats-period-btns {
|
|
1524
|
+
display: flex;
|
|
1525
|
+
gap: 4px;
|
|
1526
|
+
padding: 10px 20px;
|
|
1527
|
+
border-bottom: 1px solid var(--b1);
|
|
1528
|
+
flex-shrink: 0;
|
|
1529
|
+
}
|
|
1530
|
+
.stats-period-btn {
|
|
1531
|
+
padding: 5px 14px;
|
|
1532
|
+
border: 1px solid var(--b1);
|
|
1533
|
+
background: var(--s2);
|
|
1534
|
+
color: var(--t3);
|
|
1535
|
+
font-size: 11px;
|
|
1536
|
+
font-weight: 500;
|
|
1537
|
+
font-family: inherit;
|
|
1538
|
+
cursor: pointer;
|
|
1539
|
+
border-radius: var(--r);
|
|
1540
|
+
transition: all 0.15s;
|
|
1541
|
+
}
|
|
1542
|
+
.stats-period-btn:hover { color: var(--t1); border-color: var(--b2); }
|
|
1543
|
+
.stats-period-btn.active { background: var(--ac); color: #fff; border-color: var(--ac); }
|
|
1544
|
+
.stats-content {
|
|
1545
|
+
flex: 1;
|
|
1546
|
+
overflow-y: auto;
|
|
1547
|
+
padding: 16px 20px;
|
|
1548
|
+
}
|
|
1549
|
+
.stats-cards {
|
|
1550
|
+
display: grid;
|
|
1551
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1552
|
+
gap: 10px;
|
|
1553
|
+
margin-bottom: 20px;
|
|
1554
|
+
}
|
|
1555
|
+
.stats-card {
|
|
1556
|
+
background: var(--s2);
|
|
1557
|
+
border: 1px solid var(--b1);
|
|
1558
|
+
border-radius: var(--r);
|
|
1559
|
+
padding: 14px;
|
|
1560
|
+
text-align: center;
|
|
1561
|
+
}
|
|
1562
|
+
.stats-card-value {
|
|
1563
|
+
font-size: 24px;
|
|
1564
|
+
font-weight: 700;
|
|
1565
|
+
font-family: 'JetBrains Mono', monospace;
|
|
1566
|
+
color: var(--ac);
|
|
1567
|
+
}
|
|
1568
|
+
.stats-card-label {
|
|
1569
|
+
font-size: 10px;
|
|
1570
|
+
color: var(--t4);
|
|
1571
|
+
margin-top: 4px;
|
|
1572
|
+
text-transform: uppercase;
|
|
1573
|
+
letter-spacing: 0.5px;
|
|
1574
|
+
}
|
|
1575
|
+
.stats-section { margin-bottom: 20px; }
|
|
1576
|
+
.stats-section h4 {
|
|
1577
|
+
font-size: 11px;
|
|
1578
|
+
font-weight: 600;
|
|
1579
|
+
color: var(--t3);
|
|
1580
|
+
margin-bottom: 8px;
|
|
1581
|
+
text-transform: uppercase;
|
|
1582
|
+
letter-spacing: 0.5px;
|
|
1583
|
+
}
|
|
1584
|
+
.stats-bar-chart { display: flex; flex-direction: column; gap: 4px; }
|
|
1585
|
+
.stats-bar-row {
|
|
1586
|
+
display: flex;
|
|
1587
|
+
align-items: center;
|
|
1588
|
+
gap: 8px;
|
|
1589
|
+
font-size: 11px;
|
|
1590
|
+
}
|
|
1591
|
+
.stats-bar-label {
|
|
1592
|
+
width: 50px;
|
|
1593
|
+
text-align: right;
|
|
1594
|
+
color: var(--t4);
|
|
1595
|
+
font-family: 'JetBrains Mono', monospace;
|
|
1596
|
+
font-size: 10px;
|
|
1597
|
+
flex-shrink: 0;
|
|
1598
|
+
}
|
|
1599
|
+
.stats-bar-track {
|
|
1600
|
+
flex: 1;
|
|
1601
|
+
height: 18px;
|
|
1602
|
+
background: var(--s2);
|
|
1603
|
+
border-radius: 2px;
|
|
1604
|
+
overflow: hidden;
|
|
1605
|
+
}
|
|
1606
|
+
.stats-bar-fill {
|
|
1607
|
+
height: 100%;
|
|
1608
|
+
background: var(--ac);
|
|
1609
|
+
border-radius: 2px;
|
|
1610
|
+
transition: width 0.3s ease;
|
|
1611
|
+
min-width: 2px;
|
|
1612
|
+
}
|
|
1613
|
+
.stats-bar-count {
|
|
1614
|
+
width: 28px;
|
|
1615
|
+
text-align: left;
|
|
1616
|
+
color: var(--t3);
|
|
1617
|
+
font-family: 'JetBrains Mono', monospace;
|
|
1618
|
+
font-size: 10px;
|
|
1619
|
+
flex-shrink: 0;
|
|
1620
|
+
}
|
|
1621
|
+
.stats-agent-row {
|
|
1622
|
+
display: flex;
|
|
1623
|
+
align-items: center;
|
|
1624
|
+
gap: 8px;
|
|
1625
|
+
padding: 6px 0;
|
|
1626
|
+
border-bottom: 1px solid var(--b1);
|
|
1627
|
+
font-size: 11px;
|
|
1628
|
+
}
|
|
1629
|
+
.stats-agent-name {
|
|
1630
|
+
flex: 1;
|
|
1631
|
+
color: var(--t2);
|
|
1632
|
+
font-weight: 500;
|
|
1633
|
+
}
|
|
1634
|
+
.stats-agent-count {
|
|
1635
|
+
font-family: 'JetBrains Mono', monospace;
|
|
1636
|
+
color: var(--ac);
|
|
1637
|
+
font-weight: 600;
|
|
1638
|
+
}
|
|
1639
|
+
.stats-board-row {
|
|
1640
|
+
display: flex;
|
|
1641
|
+
justify-content: space-between;
|
|
1642
|
+
padding: 6px 0;
|
|
1643
|
+
border-bottom: 1px solid var(--b1);
|
|
1644
|
+
font-size: 11px;
|
|
1645
|
+
}
|
|
1646
|
+
.stats-board-label { color: var(--t3); }
|
|
1647
|
+
.stats-board-value { font-family: 'JetBrains Mono', monospace; color: var(--t1); font-weight: 600; }
|
|
1648
|
+
.stats-empty { text-align: center; padding: 40px 20px; color: var(--t4); font-size: 12px; }
|
|
1649
|
+
|
|
1481
1650
|
/* ── Chat Panel ── */
|
|
1482
1651
|
.chat-panel {
|
|
1483
1652
|
position: fixed;
|
|
@@ -1865,6 +2034,7 @@
|
|
|
1865
2034
|
<button onclick="toggleChat()">Chat</button>
|
|
1866
2035
|
<button class="activity-btn" onclick="toggleActivity()">Activity <span class="activity-badge" id="actBadge">0</span></button>
|
|
1867
2036
|
<button onclick="toggleArchive()">Archive</button>
|
|
2037
|
+
<button onclick="toggleStats()">Stats</button>
|
|
1868
2038
|
<button class="add-btn" onclick="openModal()">New Task</button>
|
|
1869
2039
|
<button class="clear-btn" onclick="archiveDone()">Archive Done</button>
|
|
1870
2040
|
</div>
|
|
@@ -2015,6 +2185,21 @@
|
|
|
2015
2185
|
</div>
|
|
2016
2186
|
</div>
|
|
2017
2187
|
|
|
2188
|
+
<div class="stats-overlay" id="statsOverlay" onclick="closeStats()" style="position:fixed;inset:0;background:rgba(0,0,0,0.3);z-index:305;opacity:0;pointer-events:none;transition:opacity 0.2s"></div>
|
|
2189
|
+
<div class="stats-panel" id="statsPanel">
|
|
2190
|
+
<div class="stats-header">
|
|
2191
|
+
<h3>Stats</h3>
|
|
2192
|
+
<button class="stats-close" onclick="closeStats()">×</button>
|
|
2193
|
+
</div>
|
|
2194
|
+
<div class="stats-period-btns" id="statsPeriodBtns">
|
|
2195
|
+
<button class="stats-period-btn active" onclick="loadStats('week')">Week</button>
|
|
2196
|
+
<button class="stats-period-btn" onclick="loadStats('month')">Month</button>
|
|
2197
|
+
</div>
|
|
2198
|
+
<div class="stats-content" id="statsContent">
|
|
2199
|
+
<div class="stats-empty">Loading...</div>
|
|
2200
|
+
</div>
|
|
2201
|
+
</div>
|
|
2202
|
+
|
|
2018
2203
|
<div class="chat-panel" id="chatPanel">
|
|
2019
2204
|
<div class="chat-resize-handle" id="chatResizeH"></div>
|
|
2020
2205
|
<div class="chat-resize-handle-left" id="chatResizeW"></div>
|
|
@@ -2802,9 +2987,10 @@ document.addEventListener("keydown",function(e){
|
|
|
2802
2987
|
if(e.ctrlKey&&e.shiftKey&&(e.key==="K"||e.key==="k")){e.preventDefault();stopExecution();return;}
|
|
2803
2988
|
if(e.target.closest("input,select,textarea"))return;
|
|
2804
2989
|
if(e.key==="n"||e.key==="N")openModal();
|
|
2805
|
-
if(e.key==="Escape"){if(tdCurrentId){closeTaskDetail();return;}closeModal();closeReport();closeActivity();closeArchive();closeChat();}
|
|
2990
|
+
if(e.key==="Escape"){if(tdCurrentId){closeTaskDetail();return;}closeModal();closeReport();closeActivity();closeArchive();closeStats();closeChat();}
|
|
2806
2991
|
if(e.key==="a"||e.key==="A")toggleActivity();
|
|
2807
2992
|
if(e.key==="r"||e.key==="R")toggleArchive();
|
|
2993
|
+
if(e.key==="s"||e.key==="S")toggleStats();
|
|
2808
2994
|
if(e.key==="c"||e.key==="C")toggleChat();
|
|
2809
2995
|
});
|
|
2810
2996
|
document.getElementById("modalOverlay").addEventListener("click",function(e){if(e.target===e.currentTarget)closeModal();});
|
|
@@ -3210,6 +3396,83 @@ function renderArchiveTask(t,type){
|
|
|
3210
3396
|
return h;
|
|
3211
3397
|
}
|
|
3212
3398
|
|
|
3399
|
+
// ── Stats Panel ──
|
|
3400
|
+
var statsOpen=false, statsPeriod="week", statsData=null;
|
|
3401
|
+
|
|
3402
|
+
function toggleStats(){if(statsOpen)closeStats();else openStats();}
|
|
3403
|
+
function openStats(){
|
|
3404
|
+
statsOpen=true;
|
|
3405
|
+
document.getElementById("statsPanel").classList.add("open");
|
|
3406
|
+
var ov=document.getElementById("statsOverlay");ov.style.opacity="1";ov.style.pointerEvents="auto";
|
|
3407
|
+
loadStats(statsPeriod);
|
|
3408
|
+
}
|
|
3409
|
+
function closeStats(){
|
|
3410
|
+
statsOpen=false;
|
|
3411
|
+
document.getElementById("statsPanel").classList.remove("open");
|
|
3412
|
+
var ov=document.getElementById("statsOverlay");ov.style.opacity="0";ov.style.pointerEvents="none";
|
|
3413
|
+
}
|
|
3414
|
+
function loadStats(period){
|
|
3415
|
+
statsPeriod=period;
|
|
3416
|
+
document.querySelectorAll(".stats-period-btn").forEach(function(b){b.classList.remove("active");});
|
|
3417
|
+
document.querySelector('.stats-period-btn[onclick="loadStats(\''+period+'\')"]').classList.add("active");
|
|
3418
|
+
var proj=currentProject||(projects.length?projects[0].id:"apex");
|
|
3419
|
+
fetch("/api/stats?project="+encodeURIComponent(proj)+"&period="+period).then(function(r){return r.json();}).then(function(data){
|
|
3420
|
+
statsData=data;
|
|
3421
|
+
renderStats(data);
|
|
3422
|
+
}).catch(function(){
|
|
3423
|
+
document.getElementById("statsContent").innerHTML='<div class="stats-empty">Failed to load stats</div>';
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
function renderStats(data){
|
|
3427
|
+
var container=document.getElementById("statsContent");
|
|
3428
|
+
if(!data){container.innerHTML='<div class="stats-empty">No data</div>';return;}
|
|
3429
|
+
var html='';
|
|
3430
|
+
// Summary cards
|
|
3431
|
+
html+='<div class="stats-cards">';
|
|
3432
|
+
html+='<div class="stats-card"><div class="stats-card-value">'+data.totalCompleted+'</div><div class="stats-card-label">Completed</div></div>';
|
|
3433
|
+
html+='<div class="stats-card"><div class="stats-card-value">'+(data.avgDurationMin>0?data.avgDurationMin+"m":"—")+'</div><div class="stats-card-label">Avg Time</div></div>';
|
|
3434
|
+
html+='<div class="stats-card"><div class="stats-card-value">'+data.agents.length+'</div><div class="stats-card-label">Agents</div></div>';
|
|
3435
|
+
html+='</div>';
|
|
3436
|
+
// Board status
|
|
3437
|
+
html+='<div class="stats-section"><h4>Current Board</h4>';
|
|
3438
|
+
var boardKeys=["pending","in_progress","in_review","completed"];
|
|
3439
|
+
var boardLabels={"pending":"Pending","in_progress":"In Progress","in_review":"In Review","completed":"Completed"};
|
|
3440
|
+
boardKeys.forEach(function(k){
|
|
3441
|
+
html+='<div class="stats-board-row"><span class="stats-board-label">'+boardLabels[k]+'</span><span class="stats-board-value">'+data.board[k]+'</span></div>';
|
|
3442
|
+
});
|
|
3443
|
+
html+='</div>';
|
|
3444
|
+
// Agent distribution
|
|
3445
|
+
if(data.agents.length>0){
|
|
3446
|
+
html+='<div class="stats-section"><h4>Agent Distribution</h4>';
|
|
3447
|
+
data.agents.forEach(function(a){
|
|
3448
|
+
html+='<div class="stats-agent-row"><span class="stats-agent-name">'+esc(a.agent)+'</span><span class="stats-agent-count">'+a.count+'</span></div>';
|
|
3449
|
+
});
|
|
3450
|
+
html+='</div>';
|
|
3451
|
+
}
|
|
3452
|
+
// Daily trend bar chart
|
|
3453
|
+
if(data.trend.length>0){
|
|
3454
|
+
html+='<div class="stats-section"><h4>Daily Trend</h4>';
|
|
3455
|
+
html+=renderBarChart(data.trend);
|
|
3456
|
+
html+='</div>';
|
|
3457
|
+
}
|
|
3458
|
+
container.innerHTML=html;
|
|
3459
|
+
}
|
|
3460
|
+
function renderBarChart(trend){
|
|
3461
|
+
var max=Math.max.apply(null,trend.map(function(t){return t.count;}))||1;
|
|
3462
|
+
var html='<div class="stats-bar-chart">';
|
|
3463
|
+
trend.forEach(function(day){
|
|
3464
|
+
var pct=Math.round(day.count/max*100);
|
|
3465
|
+
var label=day.date.slice(5); // MM-DD
|
|
3466
|
+
html+='<div class="stats-bar-row">';
|
|
3467
|
+
html+='<span class="stats-bar-label">'+label+'</span>';
|
|
3468
|
+
html+='<span class="stats-bar-track"><span class="stats-bar-fill" style="width:'+pct+'%"></span></span>';
|
|
3469
|
+
html+='<span class="stats-bar-count">'+day.count+'</span>';
|
|
3470
|
+
html+='</div>';
|
|
3471
|
+
});
|
|
3472
|
+
html+='</div>';
|
|
3473
|
+
return html;
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3213
3476
|
// ── Chat Panel ──
|
|
3214
3477
|
var chatMessages=[], chatOpen=false, chatStreaming=false;
|
|
3215
3478
|
|
|
@@ -43,5 +43,31 @@ You manage the kanban board, decompose tasks, assign agents, and verify results.
|
|
|
43
43
|
- 문서 작성 → doc-writer 에이전트
|
|
44
44
|
- 복합 작업 → 서브태스크로 분해 후 각 에이전트에 할당
|
|
45
45
|
|
|
46
|
+
## Repeated Task Detection
|
|
47
|
+
|
|
48
|
+
3회 이상 반복되는 작업 패턴을 감지하면 스킬로 전환을 제안한다.
|
|
49
|
+
|
|
50
|
+
### 감지 기준
|
|
51
|
+
- 동일 에이전트에게 유사 subject가 3회 이상 할당
|
|
52
|
+
- 동일 구조의 태스크가 매일 반복
|
|
53
|
+
- 아카이브에서 같은 패턴의 태스크가 3일 이상 존재
|
|
54
|
+
|
|
55
|
+
### 스킬 생성 제안
|
|
56
|
+
반복 패턴 발견 시 다음과 같이 스킬 파일을 생성한다:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
mkdir -p .claude/skills/{skill-name}
|
|
60
|
+
cat > .claude/skills/{skill-name}/SKILL.md << 'EOF'
|
|
61
|
+
# /{skill-name} — 설명
|
|
62
|
+
|
|
63
|
+
## 실행 순서
|
|
64
|
+
1. ...
|
|
65
|
+
2. ...
|
|
66
|
+
|
|
67
|
+
## 주의사항
|
|
68
|
+
- ...
|
|
69
|
+
EOF
|
|
70
|
+
```
|
|
71
|
+
|
|
46
72
|
## Learnings
|
|
47
73
|
<!-- Auto-saved orchestrator learnings below -->
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# /archive — 완료 태스크 아카이브
|
|
2
|
+
|
|
3
|
+
완료(completed) 상태의 태스크를 날짜별 아카이브로 이동한다.
|
|
4
|
+
|
|
5
|
+
## 실행 순서
|
|
6
|
+
|
|
7
|
+
1. 현재 보드 상태 확인:
|
|
8
|
+
```bash
|
|
9
|
+
curl -s http://localhost:{{port}}/api/tasks | jq '[.[] | select(.status=="completed")] | length'
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
2. 완료 태스크가 있으면 아카이브 실행:
|
|
13
|
+
```bash
|
|
14
|
+
curl -s -X POST http://localhost:{{port}}/api/archive \
|
|
15
|
+
-H "Content-Type: application/json" \
|
|
16
|
+
-d '{"project":"{{projectId}}"}'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
3. 결과 리포트:
|
|
20
|
+
- 아카이브된 태스크 수
|
|
21
|
+
- 아카이브 날짜
|
|
22
|
+
- 남은 보드 태스크 수
|
|
23
|
+
|
|
24
|
+
## 주의사항
|
|
25
|
+
- in_progress 태스크는 아카이브하지 않음
|
|
26
|
+
- 아카이브는 `.claude/tasks/_archives/` 에 날짜별 JSON으로 저장됨
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# /commit — 칸반 연동 커밋
|
|
2
|
+
|
|
3
|
+
현재 변경사항을 칸반 태스크와 연결하여 커밋한다.
|
|
4
|
+
|
|
5
|
+
## 실행 순서
|
|
6
|
+
|
|
7
|
+
1. 변경 파일 확인:
|
|
8
|
+
```bash
|
|
9
|
+
git diff --stat
|
|
10
|
+
git diff --cached --stat
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. 칸반에서 in_progress 태스크 조회:
|
|
14
|
+
```bash
|
|
15
|
+
curl -s http://localhost:{{port}}/api/tasks | jq '[.[] | select(.status=="in_progress")]'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
3. 커밋 메시지 작성:
|
|
19
|
+
- 관련 태스크가 있으면: `[#ID] 커밋 메시지`
|
|
20
|
+
- 없으면: 일반 커밋 메시지
|
|
21
|
+
- 변경 내용을 분석하여 적절한 conventional commit prefix 사용
|
|
22
|
+
|
|
23
|
+
4. 커밋 실행:
|
|
24
|
+
```bash
|
|
25
|
+
git add -A
|
|
26
|
+
git commit -m "[#ID] type: description"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
5. 태스크 업데이트 (선택):
|
|
30
|
+
- 커밋으로 태스크가 완료되면 status를 completed로 변경
|
|
31
|
+
|
|
32
|
+
## 주의사항
|
|
33
|
+
- `git diff`가 비어있으면 커밋하지 않음
|
|
34
|
+
- .env, credentials 등 민감 파일은 제외
|
|
35
|
+
- 사용자 확인 없이 push하지 않음
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# /deploy — 배포 전 칸반 상태 점검
|
|
2
|
+
|
|
3
|
+
배포 전 칸반 보드 상태를 점검하고 빌드/테스트를 확인한다.
|
|
4
|
+
|
|
5
|
+
## 실행 순서
|
|
6
|
+
|
|
7
|
+
1. 칸반 보드 상태 점검:
|
|
8
|
+
```bash
|
|
9
|
+
curl -s http://localhost:{{port}}/api/tasks
|
|
10
|
+
```
|
|
11
|
+
- in_progress 태스크가 있으면 경고
|
|
12
|
+
- pending 중 priority: high가 있으면 경고
|
|
13
|
+
|
|
14
|
+
2. 미커밋 변경 확인:
|
|
15
|
+
```bash
|
|
16
|
+
git status --porcelain
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
3. 빌드 테스트:
|
|
20
|
+
```bash
|
|
21
|
+
npm run build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
4. 테스트 실행 (있으면):
|
|
25
|
+
```bash
|
|
26
|
+
npm test 2>/dev/null || echo "No test script"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
5. 배포 준비 리포트:
|
|
30
|
+
```
|
|
31
|
+
## Deploy Readiness — {{projectName}}
|
|
32
|
+
|
|
33
|
+
### Board Status
|
|
34
|
+
- Completed: N tasks
|
|
35
|
+
- In Progress: N tasks (⚠ if > 0)
|
|
36
|
+
- Pending High: N tasks (⚠ if > 0)
|
|
37
|
+
|
|
38
|
+
### Build: ✅ / ❌
|
|
39
|
+
### Tests: ✅ / ❌ / skipped
|
|
40
|
+
### Git: clean / ⚠ uncommitted changes
|
|
41
|
+
|
|
42
|
+
### Verdict: READY / NOT READY
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
6. 사용자에게 배포 진행 여부 확인 후 실행
|
|
46
|
+
|
|
47
|
+
## 주의사항
|
|
48
|
+
- 실제 배포 명령은 사용자 확인 후에만 실행
|
|
49
|
+
- in_progress 태스크가 있으면 배포를 권장하지 않음
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# /standup — 일일 스탠드업 리포트
|
|
2
|
+
|
|
3
|
+
어제 완료한 작업과 오늘 예정 작업을 요약한다.
|
|
4
|
+
|
|
5
|
+
## 실행 순서
|
|
6
|
+
|
|
7
|
+
1. 어제 아카이브 확인:
|
|
8
|
+
```bash
|
|
9
|
+
YESTERDAY=$(date -v-1d +%Y-%m-%d 2>/dev/null || date -d "yesterday" +%Y-%m-%d)
|
|
10
|
+
curl -s "http://localhost:{{port}}/api/archives/${YESTERDAY}?project={{projectId}}"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. 오늘 보드 상태:
|
|
14
|
+
```bash
|
|
15
|
+
curl -s http://localhost:{{port}}/api/tasks
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
3. 스탠드업 포맷으로 출력:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
## Daily Standup — {{projectName}}
|
|
22
|
+
|
|
23
|
+
### Done (어제)
|
|
24
|
+
- #ID subject (agent)
|
|
25
|
+
|
|
26
|
+
### In Progress
|
|
27
|
+
- #ID subject (agent) — activeForm
|
|
28
|
+
|
|
29
|
+
### Today (pending)
|
|
30
|
+
- #ID subject (priority)
|
|
31
|
+
|
|
32
|
+
### Blockers
|
|
33
|
+
- #ID blocked by #ID
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 주의사항
|
|
37
|
+
- 아카이브가 없으면 "어제 완료 없음"으로 표시
|
|
38
|
+
- blockedBy가 있는 태스크는 Blockers 섹션에 별도 표시
|