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 CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import * as p9 from "@clack/prompts";
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 = (p10) => fs.existsSync(path.join(targetDir, p10));
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 path5 from "path";
1433
- import fs6 from "fs-extra";
1434
- import * as p8 from "@clack/prompts";
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/utils/git.ts
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 = path4.join(targetDir, ".git");
1963
- if (fs5.existsSync(gitDir)) return;
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 = path5.dirname(__filename);
2210
+ var __dirname = path9.dirname(__filename);
1973
2211
  function getTemplatesDir() {
1974
- const devPath = path5.join(__dirname, "..", "templates");
1975
- if (fs6.existsSync(devPath)) return devPath;
1976
- return path5.join(__dirname, "..", "templates");
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 = path5.resolve(answers.infra.targetDir);
2217
+ const targetDir = path9.resolve(answers.infra.targetDir);
1980
2218
  const templatesDir = getTemplatesDir();
1981
- const s = p8.spinner();
2219
+ const s = p9.spinner();
1982
2220
  s.start(t("scaffold.dirCreating"));
1983
- await fs6.ensureDir(path5.join(targetDir, "tools"));
1984
- await fs6.ensureDir(path5.join(targetDir, ".claude", "agents"));
1985
- await fs6.ensureDir(path5.join(targetDir, ".claude", "logs"));
1986
- await fs6.ensureDir(path5.join(targetDir, ".claude", "tasks", "kanban"));
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 fs6.ensureDir(path5.join(targetDir, ".claude", "tasks", proj.id));
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 = path5.join(templatesDir, "kanban.cjs");
1993
- const kanbanDest = path5.join(targetDir, "tools", "kanban.cjs");
1994
- if (fs6.existsSync(kanbanSrc)) {
1995
- await fs6.copy(kanbanSrc, kanbanDest);
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 = path5.join(templatesDir, "kanban.html");
2000
- const htmlDest = path5.join(targetDir, "tools", "kanban.html");
2001
- if (fs6.existsSync(htmlSrc)) {
2002
- await fs6.copy(htmlSrc, htmlDest);
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 fs6.writeFile(
2008
- path5.join(targetDir, "kanban.config.json"),
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 fs6.writeFile(
2018
- path5.join(targetDir, ".claude", "tasks", "_projects.json"),
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 fs6.writeFile(
2025
- path5.join(targetDir, ".claude", "agents", "_project-context.md"),
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 fs6.writeFile(
2034
- path5.join(targetDir, ".claude", "agents", `${agent}.md`),
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 = path5.join(templatesDir, "orchestrator.md");
2041
- if (fs6.existsSync(orchSrc)) {
2042
- let orchContent = await fs6.readFile(orchSrc, "utf-8");
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 fs6.writeFile(
2054
- path5.join(targetDir, ".claude", "orchestrator.md"),
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 fs6.writeFile(
2062
- path5.join(targetDir, ".env.example"),
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
- p9.intro(`${pc.bgCyan(pc.black(" create-claude-kanban "))} ${pc.dim("v2.0.0")}`);
2073
- const langChoice = await p9.select({
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 (p9.isCancel(langChoice)) {
2081
- p9.cancel("Cancelled.");
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
- p9.cancel(t("common.cancelled"));
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
- p9.note(files.join("\n"), t("index.filesHeader"));
2102
- p9.outro(`${pc.green("node tools/kanban.cjs")} ${t("index.startCmd")}`);
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
- p9.cancel(t("common.error"));
2381
+ p10.cancel(t("common.error"));
2106
2382
  console.error(err);
2107
2383
  process.exit(1);
2108
2384
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-kanban",
3
- "version": "3.2.2",
3
+ "version": "4.0.0",
4
4
  "description": "Scaffold a multi-agent kanban system for Claude Code projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
@@ -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");
@@ -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()">&times;</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 섹션에 별도 표시