claude-code-kanban 2.1.0 → 2.2.0-rc.10
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/hooks/agent-spy.sh +25 -2
- package/install.js +1 -0
- package/lib/parsers.js +37 -1
- package/package.json +2 -2
- package/public/app.js +857 -113
- package/public/index.html +36 -4
- package/public/style.css +315 -5
- package/server.js +277 -40
package/server.js
CHANGED
|
@@ -15,7 +15,8 @@ const {
|
|
|
15
15
|
readSessionInfoFromJsonl,
|
|
16
16
|
buildAgentProgressMap,
|
|
17
17
|
readCompactSummaries,
|
|
18
|
-
findTerminatedTeammates
|
|
18
|
+
findTerminatedTeammates,
|
|
19
|
+
extractPromptFromTranscript
|
|
19
20
|
} = require('./lib/parsers');
|
|
20
21
|
|
|
21
22
|
const isSetupCommand = process.argv.includes('--install') || process.argv.includes('--uninstall');
|
|
@@ -61,6 +62,11 @@ const SESSION_STALE_MS = 300000;
|
|
|
61
62
|
|
|
62
63
|
const WAITING_RESOLVE_GRACE_MS = 15000;
|
|
63
64
|
|
|
65
|
+
function persistAgent(dir, agent) {
|
|
66
|
+
const file = path.join(dir, agent.agentId + '.json');
|
|
67
|
+
fs.writeFile(file, JSON.stringify(agent), 'utf8').catch(() => {});
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
function checkWaitingForUser(agentDir, logMtime) {
|
|
65
71
|
try {
|
|
66
72
|
const data = JSON.parse(readFileSync(path.join(agentDir, '_waiting.json'), 'utf8'));
|
|
@@ -189,6 +195,9 @@ const terminatedCache = new Map();
|
|
|
189
195
|
const compactSummaryCache = new Map();
|
|
190
196
|
const taskCountsCache = new Map();
|
|
191
197
|
const contextStatusCache = new Map();
|
|
198
|
+
const TASK_MAPS_DIR = path.join(AGENT_ACTIVITY_DIR, '_task-maps');
|
|
199
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
200
|
+
function isUUID(s) { return UUID_RE.test(s); }
|
|
192
201
|
|
|
193
202
|
function evictStaleCache(cache) {
|
|
194
203
|
if (cache.size <= MAX_CACHE_ENTRIES) return;
|
|
@@ -196,6 +205,62 @@ function evictStaleCache(cache) {
|
|
|
196
205
|
if (oldest !== undefined) cache.delete(oldest);
|
|
197
206
|
}
|
|
198
207
|
|
|
208
|
+
let sessionToTaskListCache = null;
|
|
209
|
+
let lastTaskMapScan = 0;
|
|
210
|
+
const TASK_MAP_SCAN_TTL = 5000;
|
|
211
|
+
|
|
212
|
+
function loadAllTaskMaps() {
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
if (sessionToTaskListCache && now - lastTaskMapScan < TASK_MAP_SCAN_TTL) return sessionToTaskListCache;
|
|
215
|
+
|
|
216
|
+
const sessionToList = {};
|
|
217
|
+
const listToSessions = {};
|
|
218
|
+
if (!existsSync(TASK_MAPS_DIR)) {
|
|
219
|
+
sessionToTaskListCache = { sessionToList, listToSessions };
|
|
220
|
+
lastTaskMapScan = now;
|
|
221
|
+
return sessionToTaskListCache;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
for (const file of readdirSync(TASK_MAPS_DIR).filter(f => f.endsWith('.json'))) {
|
|
225
|
+
const taskListName = file.replace(/\.json$/, '');
|
|
226
|
+
const mapPath = path.join(TASK_MAPS_DIR, file);
|
|
227
|
+
try {
|
|
228
|
+
const map = JSON.parse(readFileSync(mapPath, 'utf8'));
|
|
229
|
+
listToSessions[taskListName] = map;
|
|
230
|
+
for (const sessionId of Object.keys(map)) {
|
|
231
|
+
sessionToList[sessionId] = taskListName;
|
|
232
|
+
}
|
|
233
|
+
} catch (e) { /* skip invalid */ }
|
|
234
|
+
}
|
|
235
|
+
} catch (e) { /* ignore */ }
|
|
236
|
+
sessionToTaskListCache = { sessionToList, listToSessions };
|
|
237
|
+
lastTaskMapScan = now;
|
|
238
|
+
return sessionToTaskListCache;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getCustomTaskDir(sessionId) {
|
|
242
|
+
const { sessionToList } = loadAllTaskMaps();
|
|
243
|
+
const taskListName = sessionToList[sessionId];
|
|
244
|
+
if (taskListName) {
|
|
245
|
+
const dir = path.join(TASKS_DIR, taskListName);
|
|
246
|
+
if (existsSync(dir)) return dir;
|
|
247
|
+
}
|
|
248
|
+
// Check team-named task directory (teams store tasks under ~/.claude/tasks/<teamName>/)
|
|
249
|
+
if (existsSync(TEAMS_DIR)) {
|
|
250
|
+
try {
|
|
251
|
+
for (const dir of readdirSync(TEAMS_DIR, { withFileTypes: true })) {
|
|
252
|
+
if (!dir.isDirectory()) continue;
|
|
253
|
+
const cfg = loadTeamConfig(dir.name);
|
|
254
|
+
if (cfg?.leadSessionId === sessionId) {
|
|
255
|
+
const teamTaskDir = path.join(TASKS_DIR, dir.name);
|
|
256
|
+
if (existsSync(teamTaskDir)) return teamTaskDir;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch (_) {}
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
199
264
|
function getTaskCounts(sessionPath) {
|
|
200
265
|
const cached = taskCountsCache.get(sessionPath);
|
|
201
266
|
if (cached) return cached;
|
|
@@ -449,7 +514,7 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
449
514
|
const entries = readdirSync(TASKS_DIR, { withFileTypes: true });
|
|
450
515
|
|
|
451
516
|
for (const entry of entries) {
|
|
452
|
-
if (entry.isDirectory()) {
|
|
517
|
+
if (entry.isDirectory() && isUUID(entry.name)) {
|
|
453
518
|
const sessionPath = path.join(TASKS_DIR, entry.name);
|
|
454
519
|
const stat = statSync(sessionPath);
|
|
455
520
|
const { taskCount, completed, inProgress, pending, newestTaskMtime } = getTaskCounts(sessionPath);
|
|
@@ -497,6 +562,55 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
497
562
|
}));
|
|
498
563
|
}
|
|
499
564
|
}
|
|
565
|
+
|
|
566
|
+
// Process custom task lists (non-UUID directories mapped via _task-maps)
|
|
567
|
+
const { listToSessions } = loadAllTaskMaps();
|
|
568
|
+
for (const [taskListName, map] of Object.entries(listToSessions)) {
|
|
569
|
+
const customTaskDir = path.join(TASKS_DIR, taskListName);
|
|
570
|
+
if (!existsSync(customTaskDir)) continue;
|
|
571
|
+
const counts = getTaskCounts(customTaskDir);
|
|
572
|
+
|
|
573
|
+
for (const [sessionId, info] of Object.entries(map)) {
|
|
574
|
+
const existing = sessionsMap.get(sessionId);
|
|
575
|
+
if (existing) {
|
|
576
|
+
Object.assign(existing, {
|
|
577
|
+
taskCount: counts.taskCount,
|
|
578
|
+
completed: counts.completed,
|
|
579
|
+
inProgress: counts.inProgress,
|
|
580
|
+
pending: counts.pending,
|
|
581
|
+
tasksDir: customTaskDir,
|
|
582
|
+
sharedTaskList: taskListName,
|
|
583
|
+
});
|
|
584
|
+
} else {
|
|
585
|
+
const meta = { ...(metadata[sessionId] || {}) };
|
|
586
|
+
if (!meta.project && info.project) meta.project = info.project;
|
|
587
|
+
const logStat = getSessionLogStat(meta);
|
|
588
|
+
const logMtime = logStat.mtime;
|
|
589
|
+
const logAge = logMtime ? Date.now() - logMtime : Infinity;
|
|
590
|
+
const stale = logAge > AGENT_STALE_MS;
|
|
591
|
+
const agentDir = path.join(AGENT_ACTIVITY_DIR, sessionId);
|
|
592
|
+
const agentStatus = checkAgentStatus(agentDir, stale, logMtime, false);
|
|
593
|
+
let modifiedAt = info.updatedAt || new Date(0).toISOString();
|
|
594
|
+
if (logMtime) {
|
|
595
|
+
const jsonlMtime = new Date(logMtime).toISOString();
|
|
596
|
+
if (jsonlMtime > modifiedAt) modifiedAt = jsonlMtime;
|
|
597
|
+
}
|
|
598
|
+
sessionsMap.set(sessionId, buildSessionObject(sessionId, meta, {
|
|
599
|
+
_logStat: logStat,
|
|
600
|
+
taskCount: counts.taskCount,
|
|
601
|
+
completed: counts.completed,
|
|
602
|
+
inProgress: counts.inProgress,
|
|
603
|
+
pending: counts.pending,
|
|
604
|
+
modifiedAt,
|
|
605
|
+
hasActiveAgents: agentStatus.hasActive,
|
|
606
|
+
hasRunningAgents: agentStatus.hasRunning,
|
|
607
|
+
hasWaitingForUser: !!agentStatus.waitingForUser,
|
|
608
|
+
tasksDir: customTaskDir,
|
|
609
|
+
sharedTaskList: taskListName,
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
500
614
|
}
|
|
501
615
|
|
|
502
616
|
// Add sessions from metadata that don't have task directories
|
|
@@ -544,18 +658,45 @@ app.get('/api/sessions', async (req, res) => {
|
|
|
544
658
|
} catch (e) { /* ignore */ }
|
|
545
659
|
}
|
|
546
660
|
|
|
547
|
-
//
|
|
661
|
+
// Enrich leader sessions with team info and remove team-named duplicates
|
|
548
662
|
const teamLeaderIds = new Set();
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
663
|
+
if (existsSync(TEAMS_DIR)) {
|
|
664
|
+
try {
|
|
665
|
+
for (const dir of readdirSync(TEAMS_DIR, { withFileTypes: true })) {
|
|
666
|
+
if (!dir.isDirectory()) continue;
|
|
667
|
+
// Remove team-named duplicate session (e.g., "code-review") — leader is the canonical entry
|
|
668
|
+
if (sessionsMap.has(dir.name)) sessionsMap.delete(dir.name);
|
|
669
|
+
const cfg = loadTeamConfig(dir.name);
|
|
670
|
+
if (!cfg?.leadSessionId) continue;
|
|
671
|
+
const leaderId = cfg.leadSessionId;
|
|
672
|
+
const existing = sessionsMap.get(leaderId);
|
|
673
|
+
if (existing) {
|
|
674
|
+
existing.isTeam = true;
|
|
675
|
+
existing.teamName = dir.name;
|
|
676
|
+
existing.memberCount = cfg.members?.length || 0;
|
|
677
|
+
existing.name = existing.name || cfg.name || dir.name;
|
|
678
|
+
teamLeaderIds.add(leaderId);
|
|
679
|
+
// Attach team-named task directory if present
|
|
680
|
+
const teamTaskDir = path.join(TASKS_DIR, dir.name);
|
|
681
|
+
if (!existing.tasksDir && existsSync(teamTaskDir)) {
|
|
682
|
+
const counts = getTaskCounts(teamTaskDir);
|
|
683
|
+
existing.taskCount = counts.taskCount;
|
|
684
|
+
existing.completed = counts.completed;
|
|
685
|
+
existing.inProgress = counts.inProgress;
|
|
686
|
+
existing.pending = counts.pending;
|
|
687
|
+
existing.tasksDir = teamTaskDir;
|
|
688
|
+
existing.sharedTaskList = dir.name;
|
|
689
|
+
}
|
|
690
|
+
// Re-check agent status with isTeam=true
|
|
691
|
+
const agentDir = path.join(AGENT_ACTIVITY_DIR, leaderId);
|
|
692
|
+
const logStat = getSessionLogStat(metadata[leaderId] || {});
|
|
693
|
+
const logAge = logStat.mtime ? Date.now() - logStat.mtime : Infinity;
|
|
694
|
+
const agentStatus = checkAgentStatus(agentDir, logAge > AGENT_STALE_MS, logStat.mtime, true);
|
|
695
|
+
existing.hasActiveAgents = agentStatus.hasActive;
|
|
696
|
+
existing.hasRunningAgents = agentStatus.hasRunning;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
} catch (_) {}
|
|
559
700
|
}
|
|
560
701
|
|
|
561
702
|
// Correlate plan sessions with their implementation sessions (same slug)
|
|
@@ -645,7 +786,8 @@ app.get('/api/projects', (req, res) => {
|
|
|
645
786
|
// API: Get tasks for a session
|
|
646
787
|
app.get('/api/sessions/:sessionId', async (req, res) => {
|
|
647
788
|
try {
|
|
648
|
-
const
|
|
789
|
+
const customDir = getCustomTaskDir(req.params.sessionId);
|
|
790
|
+
const sessionPath = customDir || path.join(TASKS_DIR, req.params.sessionId);
|
|
649
791
|
|
|
650
792
|
if (!existsSync(sessionPath)) {
|
|
651
793
|
return res.status(404).json({ error: 'Session not found' });
|
|
@@ -673,6 +815,52 @@ app.get('/api/sessions/:sessionId', async (req, res) => {
|
|
|
673
815
|
}
|
|
674
816
|
});
|
|
675
817
|
|
|
818
|
+
// API: Get combined tasks for a project (all sessions + shared task lists)
|
|
819
|
+
app.get('/api/projects/:encodedPath/tasks', (req, res) => {
|
|
820
|
+
try {
|
|
821
|
+
const projectPath = Buffer.from(req.params.encodedPath, 'base64').toString('utf8');
|
|
822
|
+
const metadata = loadSessionMetadata();
|
|
823
|
+
const { sessionToList } = loadAllTaskMaps();
|
|
824
|
+
|
|
825
|
+
const projectSessionIds = Object.entries(metadata)
|
|
826
|
+
.filter(([, m]) => m.project === projectPath)
|
|
827
|
+
.map(([id]) => id);
|
|
828
|
+
|
|
829
|
+
const taskDirs = new Set();
|
|
830
|
+
for (const sid of projectSessionIds) {
|
|
831
|
+
const listName = sessionToList[sid];
|
|
832
|
+
if (listName) {
|
|
833
|
+
const dir = path.join(TASKS_DIR, listName);
|
|
834
|
+
if (existsSync(dir)) taskDirs.add(dir);
|
|
835
|
+
} else {
|
|
836
|
+
const dir = path.join(TASKS_DIR, sid);
|
|
837
|
+
if (existsSync(dir)) taskDirs.add(dir);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const tasks = [];
|
|
842
|
+
const seenKeys = new Set();
|
|
843
|
+
for (const dir of taskDirs) {
|
|
844
|
+
for (const file of readdirSync(dir).filter(f => f.endsWith('.json'))) {
|
|
845
|
+
try {
|
|
846
|
+
const task = JSON.parse(readFileSync(path.join(dir, file), 'utf8'));
|
|
847
|
+
const key = `${dir}:${task.id}`;
|
|
848
|
+
if (!seenKeys.has(key)) {
|
|
849
|
+
seenKeys.add(key);
|
|
850
|
+
task._taskDir = path.basename(dir);
|
|
851
|
+
tasks.push(task);
|
|
852
|
+
}
|
|
853
|
+
} catch (_) {}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
tasks.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
|
857
|
+
res.json(tasks);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
console.error('Error getting project tasks:', error);
|
|
860
|
+
res.status(500).json({ error: 'Failed to get project tasks' });
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
|
|
676
864
|
// API: Get session plan
|
|
677
865
|
app.get('/api/sessions/:sessionId/plan', async (req, res) => {
|
|
678
866
|
try {
|
|
@@ -813,31 +1001,54 @@ app.get('/api/sessions/:sessionId/agents', (req, res) => {
|
|
|
813
1001
|
if (terminatedAt && agent.startedAt && terminatedAt < agent.startedAt) continue;
|
|
814
1002
|
agent.status = 'stopped';
|
|
815
1003
|
agent.stoppedAt = agent.stoppedAt || new Date().toISOString();
|
|
816
|
-
|
|
817
|
-
fs.writeFile(agentFile, JSON.stringify(agent), 'utf8').catch(() => {});
|
|
1004
|
+
persistAgent(agentDir, agent);
|
|
818
1005
|
}
|
|
819
1006
|
}
|
|
820
1007
|
}
|
|
821
1008
|
} catch (_) {}
|
|
822
1009
|
}
|
|
823
1010
|
|
|
1011
|
+
function persistPrompt(agent, prompt) {
|
|
1012
|
+
agent.prompt = prompt;
|
|
1013
|
+
persistAgent(agentDir, agent);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
824
1016
|
const agentsNeedingPrompt = agents.filter(a => !a.prompt);
|
|
825
1017
|
if (agentsNeedingPrompt.length && meta.jsonlPath) {
|
|
1018
|
+
let byAgentId = {};
|
|
826
1019
|
try {
|
|
827
1020
|
const progressMap = getProgressMap(meta.jsonlPath);
|
|
828
|
-
const byAgentId = {};
|
|
829
1021
|
for (const entry of Object.values(progressMap)) {
|
|
830
1022
|
if (entry.prompt && !byAgentId[entry.agentId]) byAgentId[entry.agentId] = entry.prompt;
|
|
831
1023
|
}
|
|
832
|
-
for (const agent of agentsNeedingPrompt) {
|
|
833
|
-
const prompt = byAgentId[agent.agentId];
|
|
834
|
-
if (prompt) {
|
|
835
|
-
agent.prompt = prompt;
|
|
836
|
-
const agentFile = path.join(agentDir, agent.agentId + '.json');
|
|
837
|
-
fs.writeFile(agentFile, JSON.stringify(agent), 'utf8').catch(() => {});
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
1024
|
} catch (_) {}
|
|
1025
|
+
for (const agent of agentsNeedingPrompt) {
|
|
1026
|
+
const prompt = byAgentId[agent.agentId]
|
|
1027
|
+
|| (() => { try { return extractPromptFromTranscript(subagentJsonlPath(meta, agent.agentId)); } catch (_) { return null; } })();
|
|
1028
|
+
if (prompt) persistPrompt(agent, prompt);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const agentsNeedingModel = agents.filter(a => !a.model);
|
|
1033
|
+
if (agentsNeedingModel.length && meta.jsonlPath) {
|
|
1034
|
+
for (const agent of agentsNeedingModel) {
|
|
1035
|
+
try {
|
|
1036
|
+
const jsonl = subagentJsonlPath(meta, agent.agentId);
|
|
1037
|
+
const content = readFileSync(jsonl, 'utf8');
|
|
1038
|
+
for (const line of content.split('\n')) {
|
|
1039
|
+
if (!line.trim()) continue;
|
|
1040
|
+
try {
|
|
1041
|
+
const obj = JSON.parse(line);
|
|
1042
|
+
const model = obj.model || (obj.message && obj.message.model);
|
|
1043
|
+
if (model) {
|
|
1044
|
+
agent.model = model;
|
|
1045
|
+
persistAgent(agentDir, agent);
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
} catch (_) {}
|
|
1049
|
+
}
|
|
1050
|
+
} catch (_) {}
|
|
1051
|
+
}
|
|
841
1052
|
}
|
|
842
1053
|
const teamColors = {};
|
|
843
1054
|
if (teamConfig?.members) {
|
|
@@ -868,7 +1079,7 @@ app.post('/api/sessions/:sessionId/agents/:agentId/stop', (req, res) => {
|
|
|
868
1079
|
const agent = JSON.parse(readFileSync(agentFile, 'utf8'));
|
|
869
1080
|
agent.status = 'stopped';
|
|
870
1081
|
agent.stoppedAt = new Date().toISOString();
|
|
871
|
-
writeFileSync(agentFile, JSON.stringify(agent), 'utf8');
|
|
1082
|
+
writeFileSync(agentFile, JSON.stringify(agent), 'utf8'); // sync — response depends on write
|
|
872
1083
|
// Also remove waiting state if present
|
|
873
1084
|
const waitingFile = path.join(AGENT_ACTIVITY_DIR, sessionId, '_waiting.json');
|
|
874
1085
|
if (existsSync(waitingFile)) unlinkSync(waitingFile);
|
|
@@ -980,7 +1191,7 @@ app.get('/api/sessions/:sessionId/messages', (req, res) => {
|
|
|
980
1191
|
const prompt = msg.agentPrompt || entry.prompt;
|
|
981
1192
|
if (prompt && !agent.prompt) {
|
|
982
1193
|
agent.prompt = prompt;
|
|
983
|
-
|
|
1194
|
+
persistAgent(agentDir, agent);
|
|
984
1195
|
}
|
|
985
1196
|
} catch (_) {}
|
|
986
1197
|
}
|
|
@@ -1066,7 +1277,8 @@ app.post('/api/tasks/:sessionId/:taskId/note', async (req, res) => {
|
|
|
1066
1277
|
return res.status(400).json({ error: 'Note cannot be empty' });
|
|
1067
1278
|
}
|
|
1068
1279
|
|
|
1069
|
-
const
|
|
1280
|
+
const sessionDir = getCustomTaskDir(sessionId) || path.join(TASKS_DIR, sessionId);
|
|
1281
|
+
const taskPath = path.join(sessionDir, `${taskId}.json`);
|
|
1070
1282
|
|
|
1071
1283
|
if (!existsSync(taskPath)) {
|
|
1072
1284
|
return res.status(404).json({ error: 'Task not found' });
|
|
@@ -1095,7 +1307,8 @@ app.put('/api/tasks/:sessionId/:taskId', async (req, res) => {
|
|
|
1095
1307
|
const { sessionId, taskId } = req.params;
|
|
1096
1308
|
const { subject, description } = req.body;
|
|
1097
1309
|
|
|
1098
|
-
const
|
|
1310
|
+
const sessionDir = getCustomTaskDir(sessionId) || path.join(TASKS_DIR, sessionId);
|
|
1311
|
+
const taskPath = path.join(sessionDir, `${taskId}.json`);
|
|
1099
1312
|
|
|
1100
1313
|
if (!existsSync(taskPath)) {
|
|
1101
1314
|
return res.status(404).json({ error: 'Task not found' });
|
|
@@ -1120,14 +1333,14 @@ app.put('/api/tasks/:sessionId/:taskId', async (req, res) => {
|
|
|
1120
1333
|
app.delete('/api/tasks/:sessionId/:taskId', async (req, res) => {
|
|
1121
1334
|
try {
|
|
1122
1335
|
const { sessionId, taskId } = req.params;
|
|
1123
|
-
const
|
|
1336
|
+
const sessionPath = getCustomTaskDir(sessionId) || path.join(TASKS_DIR, sessionId);
|
|
1337
|
+
const taskPath = path.join(sessionPath, `${taskId}.json`);
|
|
1124
1338
|
|
|
1125
1339
|
if (!existsSync(taskPath)) {
|
|
1126
1340
|
return res.status(404).json({ error: 'Task not found' });
|
|
1127
1341
|
}
|
|
1128
1342
|
|
|
1129
1343
|
// Check if this task blocks other tasks
|
|
1130
|
-
const sessionPath = path.join(TASKS_DIR, sessionId);
|
|
1131
1344
|
const taskFiles = readdirSync(sessionPath).filter(f => f.endsWith('.json'));
|
|
1132
1345
|
|
|
1133
1346
|
for (const file of taskFiles) {
|
|
@@ -1200,21 +1413,44 @@ const watcher = chokidar.watch(TASKS_DIR, {
|
|
|
1200
1413
|
watcher.on('all', (event, filePath) => {
|
|
1201
1414
|
if ((event === 'add' || event === 'change' || event === 'unlink') && filePath.endsWith('.json')) {
|
|
1202
1415
|
const relativePath = path.relative(TASKS_DIR, filePath);
|
|
1203
|
-
const
|
|
1416
|
+
const dirName = relativePath.split(path.sep)[0];
|
|
1204
1417
|
|
|
1205
|
-
taskCountsCache.delete(path.join(TASKS_DIR,
|
|
1418
|
+
taskCountsCache.delete(path.join(TASKS_DIR, dirName));
|
|
1206
1419
|
|
|
1207
|
-
|
|
1208
|
-
type: 'update',
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
});
|
|
1420
|
+
if (isUUID(dirName)) {
|
|
1421
|
+
broadcast({ type: 'update', event, sessionId: dirName, file: path.basename(filePath) });
|
|
1422
|
+
} else {
|
|
1423
|
+
broadcastToMappedSessions(dirName, event, filePath);
|
|
1424
|
+
}
|
|
1213
1425
|
}
|
|
1214
1426
|
});
|
|
1215
1427
|
|
|
1428
|
+
function broadcastToMappedSessions(taskListName, event, filePath) {
|
|
1429
|
+
const { listToSessions } = loadAllTaskMaps();
|
|
1430
|
+
const map = listToSessions[taskListName];
|
|
1431
|
+
if (!map) return;
|
|
1432
|
+
for (const sid of Object.keys(map)) {
|
|
1433
|
+
broadcast({ type: 'update', event, sessionId: sid, file: path.basename(filePath) });
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1216
1437
|
console.log(`Watching for changes in: ${TASKS_DIR}`);
|
|
1217
1438
|
|
|
1439
|
+
// Watch task maps directory for session→task-list mapping changes
|
|
1440
|
+
const taskMapsWatcher = chokidar.watch(TASK_MAPS_DIR, {
|
|
1441
|
+
persistent: true,
|
|
1442
|
+
ignoreInitial: true,
|
|
1443
|
+
depth: 1
|
|
1444
|
+
});
|
|
1445
|
+
taskMapsWatcher.on('all', (event, filePath) => {
|
|
1446
|
+
if ((event === 'add' || event === 'change' || event === 'unlink') && filePath.endsWith('.json')) {
|
|
1447
|
+
lastTaskMapScan = 0;
|
|
1448
|
+
const taskListName = path.basename(filePath, '.json');
|
|
1449
|
+
taskCountsCache.delete(path.join(TASKS_DIR, taskListName));
|
|
1450
|
+
broadcastToMappedSessions(taskListName, event, filePath);
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1218
1454
|
// Watch teams directory for config changes
|
|
1219
1455
|
const teamsWatcher = chokidar.watch(TEAMS_DIR, {
|
|
1220
1456
|
persistent: true,
|
|
@@ -1269,7 +1505,8 @@ plansWatcher.on('all', (event, filePath) => {
|
|
|
1269
1505
|
const agentActivityWatcher = chokidar.watch(AGENT_ACTIVITY_DIR, {
|
|
1270
1506
|
persistent: true,
|
|
1271
1507
|
ignoreInitial: true,
|
|
1272
|
-
depth: 2
|
|
1508
|
+
depth: 2,
|
|
1509
|
+
awaitWriteFinish: { stabilityThreshold: 150, pollInterval: 50 }
|
|
1273
1510
|
});
|
|
1274
1511
|
|
|
1275
1512
|
const AGENT_FILE_CAP = 20;
|