create-claude-kanban 2.0.4 → 3.1.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 +1066 -379
- package/package.json +1 -1
- package/templates/kanban.cjs +197 -1
- package/templates/kanban.html +569 -4
package/package.json
CHANGED
package/templates/kanban.cjs
CHANGED
|
@@ -151,6 +151,60 @@ function getThreadTs(taskId) {
|
|
|
151
151
|
let chatSessionId = null;
|
|
152
152
|
let chatSessionModel = null;
|
|
153
153
|
|
|
154
|
+
// ── Agent Activity Tracker (v3: zombie detection) ──
|
|
155
|
+
var agentTracker = {}; // taskId -> { pid, startedAt, lastActivity, agent, subject }
|
|
156
|
+
var STALE_THRESHOLD = 30 * 60 * 1000; // 30 min
|
|
157
|
+
|
|
158
|
+
function trackAgent(taskId, pid, agent, subject) {
|
|
159
|
+
agentTracker[String(taskId)] = { pid: pid, startedAt: Date.now(), lastActivity: Date.now(), agent: agent || "", subject: subject || "" };
|
|
160
|
+
}
|
|
161
|
+
function touchAgent(taskId) {
|
|
162
|
+
if (agentTracker[String(taskId)]) agentTracker[String(taskId)].lastActivity = Date.now();
|
|
163
|
+
}
|
|
164
|
+
function untrackAgent(taskId) {
|
|
165
|
+
delete agentTracker[String(taskId)];
|
|
166
|
+
}
|
|
167
|
+
function isProcessAlive(pid) {
|
|
168
|
+
try { process.kill(pid, 0); return true; } catch (e) { return false; }
|
|
169
|
+
}
|
|
170
|
+
function checkZombies() {
|
|
171
|
+
var now = Date.now();
|
|
172
|
+
var zombies = [];
|
|
173
|
+
Object.keys(agentTracker).forEach(function (tid) {
|
|
174
|
+
var info = agentTracker[tid];
|
|
175
|
+
var alive = isProcessAlive(info.pid);
|
|
176
|
+
var stale = (now - info.lastActivity) > STALE_THRESHOLD;
|
|
177
|
+
if (!alive || stale) {
|
|
178
|
+
zombies.push({ taskId: tid, reason: !alive ? "process_dead" : "stale", agent: info.agent, subject: info.subject, lastActivity: info.lastActivity, startedAt: info.startedAt });
|
|
179
|
+
if (!alive) {
|
|
180
|
+
untrackAgent(tid);
|
|
181
|
+
// Auto-mark stale in task file
|
|
182
|
+
try {
|
|
183
|
+
var t = readTask(tid);
|
|
184
|
+
if (t && t.status === "in_progress") {
|
|
185
|
+
updateTask(tid, { _stale: true, _staleReason: "process_dead", _staleAt: new Date().toISOString() });
|
|
186
|
+
logActivity({ type: "updated", taskId: tid, subject: info.subject, detail: "Zombie detected: agent process dead" });
|
|
187
|
+
broadcastRaw({ type: "zombie", taskId: tid, reason: "process_dead" });
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {}
|
|
190
|
+
} else if (stale) {
|
|
191
|
+
try {
|
|
192
|
+
var t = readTask(tid);
|
|
193
|
+
if (t && t.status === "in_progress" && !t._stale) {
|
|
194
|
+
updateTask(tid, { _stale: true, _staleReason: "timeout", _staleAt: new Date().toISOString() });
|
|
195
|
+
logActivity({ type: "updated", taskId: tid, subject: info.subject, detail: "Zombie detected: no activity for 30min" });
|
|
196
|
+
broadcastRaw({ type: "zombie", taskId: tid, reason: "stale" });
|
|
197
|
+
}
|
|
198
|
+
} catch (e) {}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
return zombies;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Run zombie check every 60 seconds
|
|
206
|
+
setInterval(checkZombies, 60000);
|
|
207
|
+
|
|
154
208
|
// ── Orchestrator Prompt System ──
|
|
155
209
|
const ORCHESTRATOR_FILE = path.join(os.homedir(), ".claude", "orchestrator.md");
|
|
156
210
|
const ORCHESTRATOR_LOG = path.join(os.homedir(), ".claude", "orchestrator-history.jsonl");
|
|
@@ -546,6 +600,11 @@ function readActivity(since, limit) {
|
|
|
546
600
|
}
|
|
547
601
|
|
|
548
602
|
// ── Task 파일 읽기 ──
|
|
603
|
+
function readTask(id) {
|
|
604
|
+
var all = readAllTasks();
|
|
605
|
+
return all.find(function(t) { return String(t.id) === String(id); }) || null;
|
|
606
|
+
}
|
|
607
|
+
|
|
549
608
|
function readAllTasks(projectFilter) {
|
|
550
609
|
const tasks = [];
|
|
551
610
|
if (!fs.existsSync(TASKS_DIR)) return tasks;
|
|
@@ -684,6 +743,9 @@ function updateTask(id, data) {
|
|
|
684
743
|
if (data.reportPath !== undefined) task.reportPath = data.reportPath;
|
|
685
744
|
if (data.reportSummary !== undefined) task.reportSummary = data.reportSummary;
|
|
686
745
|
if (data.parentId !== undefined) task.parentId = data.parentId;
|
|
746
|
+
if (data._stale !== undefined) task._stale = data._stale;
|
|
747
|
+
if (data._staleReason !== undefined) task._staleReason = data._staleReason;
|
|
748
|
+
if (data._staleAt !== undefined) task._staleAt = data._staleAt;
|
|
687
749
|
task.updatedAt = now;
|
|
688
750
|
fs.writeFileSync(filePath, JSON.stringify(task, null, 2));
|
|
689
751
|
|
|
@@ -873,8 +935,18 @@ function buildExecutorPrompt(task) {
|
|
|
873
935
|
if (agentPrompt) parts.push(agentPrompt);
|
|
874
936
|
|
|
875
937
|
parts.push("## 태스크\n\n- **Task ID**: " + task.id + "\n- **Subject**: " + task.subject + "\n\n" + (task.description || "(설명 없음)"));
|
|
938
|
+
|
|
939
|
+
// v3: Include review feedback from comments
|
|
940
|
+
if (task.comments && task.comments.length > 0) {
|
|
941
|
+
var reviewComments = task.comments.filter(function (c) { return c.author === "reviewer"; });
|
|
942
|
+
if (reviewComments.length > 0) {
|
|
943
|
+
parts.push("## 리뷰 피드백\n\n이전 실행에서 리뷰어가 남긴 피드백입니다. 반드시 반영하세요:\n\n" +
|
|
944
|
+
reviewComments.map(function (c) { return "- " + c.text + " (" + c.createdAt + ")"; }).join("\n"));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
876
948
|
parts.push("작업 디렉토리: " + path.join(__dirname, ".."));
|
|
877
|
-
parts.push("## Slack Progress Reporting\n\n진행 상황을 Slack에 보고할 때 반드시 아래 API를 사용한다:\n\n```bash\ncurl -s -X POST http://localhost:
|
|
949
|
+
parts.push("## Slack Progress Reporting\n\n진행 상황을 Slack에 보고할 때 반드시 아래 API를 사용한다:\n\n```bash\ncurl -s -X POST http://localhost:" + PORT + "/api/tasks/" + task.id + "/slack \\\n -H \"Content-Type: application/json\" \\\n -d '{\"text\":\"progress message\"}'\n```\n\n`$SLACK_AGENT_WEBHOOK` 직접 사용 금지. 반드시 위 API를 통해 보고.");
|
|
878
950
|
|
|
879
951
|
return parts.join("\n\n---\n\n");
|
|
880
952
|
}
|
|
@@ -895,6 +967,7 @@ function spawnExecutor(task) {
|
|
|
895
967
|
});
|
|
896
968
|
|
|
897
969
|
activeExec = { process: proc, taskId: taskId, output: "" };
|
|
970
|
+
trackAgent(taskId, proc.pid, task.agent, task.subject);
|
|
898
971
|
broadcastRaw({ type: "exec_start", taskId: taskId, subject: task.subject, agent: task.agent || "", model: model });
|
|
899
972
|
logActivity({ type: "started", taskId: taskId, subject: task.subject, detail: "Agent: " + (task.agent || "none") + " | Model: " + model });
|
|
900
973
|
|
|
@@ -918,6 +991,7 @@ function spawnExecutor(task) {
|
|
|
918
991
|
else if (json.type === "content_block_delta" && json.delta) text = json.delta.text || "";
|
|
919
992
|
if (text && activeExec) {
|
|
920
993
|
activeExec.output += text;
|
|
994
|
+
touchAgent(taskId);
|
|
921
995
|
broadcastRaw({ type: "exec", taskId: taskId, chunk: text });
|
|
922
996
|
}
|
|
923
997
|
} catch (e) {}
|
|
@@ -931,6 +1005,7 @@ function spawnExecutor(task) {
|
|
|
931
1005
|
proc.on("close", function (code) {
|
|
932
1006
|
var output = activeExec ? activeExec.output : "";
|
|
933
1007
|
activeExec = null;
|
|
1008
|
+
untrackAgent(taskId);
|
|
934
1009
|
if (code === 0) {
|
|
935
1010
|
updateTask(taskId, { status: "in_review", reportSummary: output.slice(0, 500) });
|
|
936
1011
|
broadcastRaw({ type: "exec_done", taskId: taskId, exitCode: 0 });
|
|
@@ -1526,6 +1601,94 @@ const server = http.createServer(async (req, res) => {
|
|
|
1526
1601
|
return;
|
|
1527
1602
|
}
|
|
1528
1603
|
|
|
1604
|
+
// GET /api/metrics — progress metrics
|
|
1605
|
+
if (req.url.split("?")[0] === "/api/metrics" && req.method === "GET") {
|
|
1606
|
+
var metricsParams = new URL(req.url, "http://localhost").searchParams;
|
|
1607
|
+
var mProject = metricsParams.get("project") || null;
|
|
1608
|
+
var allT = readAllTasks(mProject);
|
|
1609
|
+
var total = allT.length, pending = 0, active = 0, review = 0, completed = 0, blocked = 0, stale = 0;
|
|
1610
|
+
var byAgent = {}, byPriority = { high: 0, medium: 0, low: 0 };
|
|
1611
|
+
var dailyCompleted = {}; // date -> count
|
|
1612
|
+
var dailyCreated = {};
|
|
1613
|
+
allT.forEach(function (t) {
|
|
1614
|
+
var st = t.status || "pending";
|
|
1615
|
+
if (st === "pending") pending++;
|
|
1616
|
+
else if (st === "in_progress") active++;
|
|
1617
|
+
else if (st === "in_review") review++;
|
|
1618
|
+
else if (st === "completed") completed++;
|
|
1619
|
+
if (t.blockedBy && t.blockedBy.length > 0 && st !== "completed") blocked++;
|
|
1620
|
+
if (t._stale) stale++;
|
|
1621
|
+
var ag = t.agent || "unassigned";
|
|
1622
|
+
if (!byAgent[ag]) byAgent[ag] = { total: 0, completed: 0, in_progress: 0, pending: 0 };
|
|
1623
|
+
byAgent[ag].total++;
|
|
1624
|
+
byAgent[ag][st] = (byAgent[ag][st] || 0) + 1;
|
|
1625
|
+
byPriority[t.priority || "medium"]++;
|
|
1626
|
+
if (t.completedAt) {
|
|
1627
|
+
var d = t.completedAt.slice(0, 10);
|
|
1628
|
+
dailyCompleted[d] = (dailyCompleted[d] || 0) + 1;
|
|
1629
|
+
}
|
|
1630
|
+
if (t.createdAt) {
|
|
1631
|
+
var d2 = t.createdAt.slice(0, 10);
|
|
1632
|
+
dailyCreated[d2] = (dailyCreated[d2] || 0) + 1;
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
// Build burndown: remaining tasks over time
|
|
1636
|
+
var burndown = [];
|
|
1637
|
+
var dates = Object.keys(Object.assign({}, dailyCompleted, dailyCreated)).sort();
|
|
1638
|
+
if (dates.length > 0) {
|
|
1639
|
+
var remaining = total;
|
|
1640
|
+
for (var di = dates.length - 1; di >= 0; di--) {
|
|
1641
|
+
burndown.unshift({ date: dates[di], remaining: remaining });
|
|
1642
|
+
remaining = remaining + (dailyCompleted[dates[di]] || 0) - (dailyCreated[dates[di]] || 0);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1646
|
+
res.end(JSON.stringify({
|
|
1647
|
+
total: total, pending: pending, active: active, review: review, completed: completed,
|
|
1648
|
+
blocked: blocked, stale: stale,
|
|
1649
|
+
completionRate: total > 0 ? Math.round(completed / total * 100) : 0,
|
|
1650
|
+
byAgent: byAgent, byPriority: byPriority,
|
|
1651
|
+
dailyCompleted: dailyCompleted, dailyCreated: dailyCreated,
|
|
1652
|
+
burndown: burndown
|
|
1653
|
+
}));
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// GET /api/agents/status — agent tracker + zombie info
|
|
1658
|
+
if (req.url === "/api/agents/status" && req.method === "GET") {
|
|
1659
|
+
var agents = {};
|
|
1660
|
+
var allT = readAllTasks();
|
|
1661
|
+
// Collect agent stats
|
|
1662
|
+
allT.forEach(function(t) {
|
|
1663
|
+
var a = t.agent || "unassigned";
|
|
1664
|
+
if (!agents[a]) agents[a] = { total: 0, completed: 0, in_progress: 0, pending: 0, in_review: 0 };
|
|
1665
|
+
agents[a].total++;
|
|
1666
|
+
agents[a][t.status] = (agents[a][t.status] || 0) + 1;
|
|
1667
|
+
});
|
|
1668
|
+
// Merge live tracking
|
|
1669
|
+
var tracked = {};
|
|
1670
|
+
Object.keys(agentTracker).forEach(function(tid) {
|
|
1671
|
+
var info = agentTracker[tid];
|
|
1672
|
+
tracked[tid] = { pid: info.pid, alive: isProcessAlive(info.pid), startedAt: info.startedAt, lastActivity: info.lastActivity, agent: info.agent, subject: info.subject };
|
|
1673
|
+
});
|
|
1674
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1675
|
+
res.end(JSON.stringify({ agents: agents, tracked: tracked, activeExec: activeExec ? { taskId: activeExec.taskId } : null }));
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// POST /api/tasks/:id/recover — recover zombie task
|
|
1680
|
+
var recoverMatch = req.url.match(/^\/api\/tasks\/(\d+)\/recover$/);
|
|
1681
|
+
if (recoverMatch && req.method === "POST") {
|
|
1682
|
+
var rid = recoverMatch[1];
|
|
1683
|
+
untrackAgent(rid);
|
|
1684
|
+
updateTask(rid, { status: "pending", _stale: false, _staleReason: null, _staleAt: null, activeForm: null });
|
|
1685
|
+
logActivity({ type: "updated", taskId: rid, subject: "", detail: "Recovered from zombie state" });
|
|
1686
|
+
broadcastRaw({ type: "tasks_updated" });
|
|
1687
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1688
|
+
res.end(JSON.stringify({ ok: true, taskId: rid }));
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1529
1692
|
// GET /api/tasks
|
|
1530
1693
|
if (req.url.split("?")[0] === "/api/tasks" && req.method === "GET") {
|
|
1531
1694
|
var taskParams = new URL(req.url, "http://localhost").searchParams;
|
|
@@ -1607,6 +1770,39 @@ const server = http.createServer(async (req, res) => {
|
|
|
1607
1770
|
return;
|
|
1608
1771
|
}
|
|
1609
1772
|
|
|
1773
|
+
// GET /api/tasks/:id/comments — list comments
|
|
1774
|
+
var commentsGetMatch = req.url.match(/^\/api\/tasks\/(\d+)\/comments$/);
|
|
1775
|
+
if (commentsGetMatch && req.method === "GET") {
|
|
1776
|
+
var cid = commentsGetMatch[1];
|
|
1777
|
+
var ct = readTask(cid);
|
|
1778
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1779
|
+
res.end(JSON.stringify(ct && ct.comments ? ct.comments : []));
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// POST /api/tasks/:id/comments — add comment
|
|
1784
|
+
var commentsPostMatch = req.url.match(/^\/api\/tasks\/(\d+)\/comments$/);
|
|
1785
|
+
if (commentsPostMatch && req.method === "POST") {
|
|
1786
|
+
var cid2 = commentsPostMatch[1];
|
|
1787
|
+
collectBody(req, function (body) {
|
|
1788
|
+
try {
|
|
1789
|
+
var cdata = JSON.parse(body);
|
|
1790
|
+
var filePath = findTaskFile(cid2);
|
|
1791
|
+
if (!filePath) { res.writeHead(404); res.end(JSON.stringify({ error: "not found" })); return; }
|
|
1792
|
+
var task = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
1793
|
+
if (!task.comments) task.comments = [];
|
|
1794
|
+
var comment = { author: cdata.author || "user", text: cdata.text || "", createdAt: new Date().toISOString() };
|
|
1795
|
+
task.comments.push(comment);
|
|
1796
|
+
fs.writeFileSync(filePath, JSON.stringify(task, null, 2));
|
|
1797
|
+
broadcastRaw({ type: "tasks_updated" });
|
|
1798
|
+
logActivity({ type: "updated", taskId: cid2, subject: task.subject || "", detail: "Comment by " + comment.author });
|
|
1799
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1800
|
+
res.end(JSON.stringify({ ok: true, comment: comment }));
|
|
1801
|
+
} catch (e) { res.writeHead(400); res.end(JSON.stringify({ error: String(e) })); }
|
|
1802
|
+
});
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1610
1806
|
// POST /api/tasks/:id/slack — Agent progress reporting via thread
|
|
1611
1807
|
const slackMatch = req.url.match(/^\/api\/tasks\/([\w.-]+)\/slack$/);
|
|
1612
1808
|
if (slackMatch && req.method === "POST") {
|