crewly 1.4.82 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/domain-sops/EXAMPLE.sop.md +21 -0
- package/config/overlays/can-decide.md +5 -0
- package/config/overlays/can-delegate.md +5 -0
- package/config/overlays/can-user-reply.md +5 -0
- package/config/overlays/can-verify.md +5 -0
- package/config/risk-policies/EXAMPLE.policy.md +22 -0
- package/config/roles/developer/fragments/role-boundary.md +30 -0
- package/config/roles/orchestrator/fragments/role-boundary.md +25 -0
- package/config/roles/orchestrator/prompt.md +40 -6
- package/config/roles/team-leader/fragments/role-boundary.md +22 -0
- package/config/skills/agent/core/get-my-tasks/execute.sh +16 -0
- package/config/skills/agent/core/save-working-state/execute.sh +19 -0
- package/config/skills/agent/core/update-user-profile/execute.sh +13 -0
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts +34 -0
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.js +107 -0
- package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.routes.js +5 -1
- package/dist/backend/backend/src/controllers/chat/chat.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/index.d.ts +1 -1
- package/dist/backend/backend/src/controllers/chat/index.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/index.js +1 -1
- package/dist/backend/backend/src/controllers/chat/index.js.map +1 -1
- package/dist/backend/backend/src/controllers/memory/index.d.ts +1 -1
- package/dist/backend/backend/src/controllers/memory/index.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/memory/index.js +1 -1
- package/dist/backend/backend/src/controllers/memory/index.js.map +1 -1
- package/dist/backend/backend/src/controllers/memory/memory.controller.d.ts +35 -0
- package/dist/backend/backend/src/controllers/memory/memory.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/memory/memory.controller.js +61 -1
- package/dist/backend/backend/src/controllers/memory/memory.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/memory/memory.routes.d.ts +2 -0
- package/dist/backend/backend/src/controllers/memory/memory.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/memory/memory.routes.js +5 -1
- package/dist/backend/backend/src/controllers/memory/memory.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/system/cron-task.controller.d.ts +1 -0
- package/dist/backend/backend/src/controllers/system/cron-task.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/system/cron-task.controller.js +12 -6
- package/dist/backend/backend/src/controllers/system/cron-task.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts +24 -0
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +107 -0
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.controller.js +49 -0
- package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +46 -42
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/routes/modules/task-management.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/modules/task-management.routes.js +5 -0
- package/dist/backend/backend/src/routes/modules/task-management.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +24 -0
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +104 -2
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/capability-overlay.module.d.ts +33 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/capability-overlay.module.d.ts.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/capability-overlay.module.js +70 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/capability-overlay.module.js.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/domain-sop.module.d.ts +35 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/domain-sop.module.d.ts.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/domain-sop.module.js +50 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/domain-sop.module.js.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/index.d.ts +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/index.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/index.js +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/index.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/markdown-file-module.d.ts +46 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/markdown-file-module.d.ts.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/markdown-file-module.js +44 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/markdown-file-module.js.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/prompt-assembly.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/prompt-assembly.service.js +13 -3
- package/dist/backend/backend/src/services/ai/prompt-modules/prompt-assembly.service.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts +22 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/prompt-module.interface.js +2 -11
- package/dist/backend/backend/src/services/ai/prompt-modules/prompt-module.interface.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/recovery.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/recovery.module.js +10 -2
- package/dist/backend/backend/src/services/ai/prompt-modules/recovery.module.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/risk-policy.module.d.ts +35 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/risk-policy.module.d.ts.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/risk-policy.module.js +50 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/risk-policy.module.js.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/role-boundary.module.d.ts +67 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/role-boundary.module.d.ts.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/role-boundary.module.js +172 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/role-boundary.module.js.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/user-profile-reference.module.d.ts +5 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/user-profile-reference.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/user-profile-reference.module.js +18 -2
- package/dist/backend/backend/src/services/ai/prompt-modules/user-profile-reference.module.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-client.service.js +9 -2
- package/dist/backend/backend/src/services/cloud/cloud-client.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/relay-client.service.d.ts +11 -0
- package/dist/backend/backend/src/services/cloud/relay-client.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/relay-client.service.js +41 -5
- package/dist/backend/backend/src/services/cloud/relay-client.service.js.map +1 -1
- package/dist/backend/backend/src/services/memory/agent-memory.service.d.ts +22 -0
- package/dist/backend/backend/src/services/memory/agent-memory.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/memory/agent-memory.service.js +103 -6
- package/dist/backend/backend/src/services/memory/agent-memory.service.js.map +1 -1
- package/dist/backend/backend/src/services/memory/index.d.ts +2 -1
- package/dist/backend/backend/src/services/memory/index.d.ts.map +1 -1
- package/dist/backend/backend/src/services/memory/index.js +1 -0
- package/dist/backend/backend/src/services/memory/index.js.map +1 -1
- package/dist/backend/backend/src/services/memory/memory.service.d.ts +43 -0
- package/dist/backend/backend/src/services/memory/memory.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/memory/memory.service.js +96 -0
- package/dist/backend/backend/src/services/memory/memory.service.js.map +1 -1
- package/dist/backend/backend/src/services/memory/user-profile.service.d.ts +77 -0
- package/dist/backend/backend/src/services/memory/user-profile.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/memory/user-profile.service.js +171 -0
- package/dist/backend/backend/src/services/memory/user-profile.service.js.map +1 -0
- package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts +8 -3
- package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/queue-processor.service.js +41 -16
- package/dist/backend/backend/src/services/messaging/queue-processor.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/thread-status-queue.service.d.ts +144 -1
- package/dist/backend/backend/src/services/messaging/thread-status-queue.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/thread-status-queue.service.js +259 -2
- package/dist/backend/backend/src/services/messaging/thread-status-queue.service.js.map +1 -1
- package/dist/backend/backend/src/services/project/task-tracking.service.d.ts +58 -2
- package/dist/backend/backend/src/services/project/task-tracking.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project/task-tracking.service.js +189 -1
- package/dist/backend/backend/src/services/project/task-tracking.service.js.map +1 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.js +12 -0
- package/dist/backend/backend/src/services/workflow/cron-task.service.js.map +1 -1
- package/dist/backend/backend/src/types/event-bus.types.d.ts +1 -1
- package/dist/backend/backend/src/types/event-bus.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/event-bus.types.js +14 -0
- package/dist/backend/backend/src/types/event-bus.types.js.map +1 -1
- package/dist/backend/backend/src/types/index.d.ts +12 -0
- package/dist/backend/backend/src/types/index.d.ts.map +1 -1
- package/dist/backend/backend/src/types/index.js.map +1 -1
- package/dist/backend/backend/src/types/memory.types.d.ts +53 -0
- package/dist/backend/backend/src/types/memory.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/memory.types.js.map +1 -1
- package/dist/backend/backend/src/types/task-tracking.types.d.ts +28 -1
- package/dist/backend/backend/src/types/task-tracking.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/task-tracking.types.js.map +1 -1
- package/dist/backend/backend/src/types/thread-status.types.d.ts +4 -0
- package/dist/backend/backend/src/types/thread-status.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/thread-status.types.js +4 -1
- package/dist/backend/backend/src/types/thread-status.types.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts +157 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts.map +1 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.js +37 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.js.map +1 -0
- package/dist/cli/backend/src/services/memory/agent-memory.service.d.ts +22 -0
- package/dist/cli/backend/src/services/memory/agent-memory.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/memory/agent-memory.service.js +103 -6
- package/dist/cli/backend/src/services/memory/agent-memory.service.js.map +1 -1
- package/dist/cli/backend/src/services/memory/goal-tracking.service.d.ts +239 -0
- package/dist/cli/backend/src/services/memory/goal-tracking.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/memory/goal-tracking.service.js +353 -0
- package/dist/cli/backend/src/services/memory/goal-tracking.service.js.map +1 -0
- package/dist/cli/backend/src/services/memory/memory.service.d.ts +43 -0
- package/dist/cli/backend/src/services/memory/memory.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/memory/memory.service.js +96 -0
- package/dist/cli/backend/src/services/memory/memory.service.js.map +1 -1
- package/dist/cli/backend/src/services/project/task-tracking.service.d.ts +171 -0
- package/dist/cli/backend/src/services/project/task-tracking.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/project/task-tracking.service.js +725 -0
- package/dist/cli/backend/src/services/project/task-tracking.service.js.map +1 -0
- package/dist/cli/backend/src/types/index.d.ts +12 -0
- package/dist/cli/backend/src/types/index.d.ts.map +1 -1
- package/dist/cli/backend/src/types/index.js.map +1 -1
- package/dist/cli/backend/src/types/memory.types.d.ts +53 -0
- package/dist/cli/backend/src/types/memory.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/memory.types.js.map +1 -1
- package/dist/cli/backend/src/types/task-tracking.types.d.ts +206 -0
- package/dist/cli/backend/src/types/task-tracking.types.d.ts.map +1 -0
- package/dist/cli/backend/src/types/task-tracking.types.js +5 -0
- package/dist/cli/backend/src/types/task-tracking.types.js.map +1 -0
- package/dist/cli/backend/src/types/thread-status.types.d.ts +4 -0
- package/dist/cli/backend/src/types/thread-status.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/thread-status.types.js +4 -1
- package/dist/cli/backend/src/types/thread-status.types.js.map +1 -1
- package/frontend/dist/assets/index-512efc8e.js +4921 -0
- package/frontend/dist/assets/{index-975ccc95.css → index-dc6ac165.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-d28d1135.js +0 -5215
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as fsSync from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import { CREWLY_CONSTANTS } from '../../constants.js';
|
|
8
|
+
import { LoggerService } from '../core/logger.service.js';
|
|
9
|
+
export class TaskTrackingService extends EventEmitter {
|
|
10
|
+
taskTrackingPath;
|
|
11
|
+
logger = LoggerService.getInstance().createComponentLogger('TaskTrackingService');
|
|
12
|
+
autoSyncInterval;
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
this.taskTrackingPath = path.join(os.homedir(), '.crewly', 'in_progress_tasks.json');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Start periodic file system sync for all projects (#137).
|
|
19
|
+
* Cleans up stale task tracking entries whose files have been moved or deleted.
|
|
20
|
+
*
|
|
21
|
+
* @param intervalMs - Sync interval in milliseconds (default: 30 minutes)
|
|
22
|
+
*/
|
|
23
|
+
startAutoSync(intervalMs = 30 * 60 * 1000) {
|
|
24
|
+
if (this.autoSyncInterval) {
|
|
25
|
+
clearInterval(this.autoSyncInterval);
|
|
26
|
+
}
|
|
27
|
+
this.autoSyncInterval = setInterval(() => {
|
|
28
|
+
this.runAutoSync().catch((err) => {
|
|
29
|
+
this.logger.warn('Auto-sync failed (non-critical)', {
|
|
30
|
+
error: err instanceof Error ? err.message : String(err),
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}, intervalMs);
|
|
34
|
+
this.logger.info('Task auto-sync started', { intervalMs });
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Stop periodic file system sync.
|
|
38
|
+
*/
|
|
39
|
+
stopAutoSync() {
|
|
40
|
+
if (this.autoSyncInterval) {
|
|
41
|
+
clearInterval(this.autoSyncInterval);
|
|
42
|
+
this.autoSyncInterval = undefined;
|
|
43
|
+
this.logger.info('Task auto-sync stopped');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Run auto-sync: iterate all tracked tasks and sync with file system.
|
|
48
|
+
* Groups tasks by projectId and calls syncTasksWithFileSystem for each.
|
|
49
|
+
*/
|
|
50
|
+
async runAutoSync() {
|
|
51
|
+
const data = await this.loadTaskData();
|
|
52
|
+
if (data.tasks.length === 0)
|
|
53
|
+
return;
|
|
54
|
+
// Group tasks by projectId and extract unique project paths
|
|
55
|
+
const projectPaths = new Map();
|
|
56
|
+
for (const task of data.tasks) {
|
|
57
|
+
if (!projectPaths.has(task.projectId) && task.taskFilePath) {
|
|
58
|
+
// Derive project path from task file path:
|
|
59
|
+
// taskFilePath looks like /path/to/project/.crewly/tasks/milestone/status/file.md
|
|
60
|
+
const crewlyIdx = task.taskFilePath.indexOf('/.crewly/');
|
|
61
|
+
if (crewlyIdx > 0) {
|
|
62
|
+
projectPaths.set(task.projectId, task.taskFilePath.substring(0, crewlyIdx));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const [projectId, projectPath] of projectPaths) {
|
|
67
|
+
try {
|
|
68
|
+
await this.syncTasksWithFileSystem(projectPath, projectId);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
this.logger.warn('Sync failed for project', {
|
|
72
|
+
projectId,
|
|
73
|
+
projectPath,
|
|
74
|
+
error: err instanceof Error ? err.message : String(err),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async loadTaskData() {
|
|
80
|
+
try {
|
|
81
|
+
if (!fsSync.existsSync(this.taskTrackingPath)) {
|
|
82
|
+
const initialData = {
|
|
83
|
+
tasks: [],
|
|
84
|
+
lastUpdated: new Date().toISOString(),
|
|
85
|
+
version: '1.0.0'
|
|
86
|
+
};
|
|
87
|
+
await this.saveTaskData(initialData);
|
|
88
|
+
return initialData;
|
|
89
|
+
}
|
|
90
|
+
const content = await fs.readFile(this.taskTrackingPath, 'utf-8');
|
|
91
|
+
return JSON.parse(content);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.logger.error('Error loading task tracking data', { error: error instanceof Error ? error.message : String(error) });
|
|
95
|
+
return {
|
|
96
|
+
tasks: [],
|
|
97
|
+
lastUpdated: new Date().toISOString(),
|
|
98
|
+
version: '1.0.0'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async saveTaskData(data) {
|
|
103
|
+
try {
|
|
104
|
+
data.lastUpdated = new Date().toISOString();
|
|
105
|
+
await fs.writeFile(this.taskTrackingPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
this.logger.error('Error saving task tracking data', { error: error instanceof Error ? error.message : String(error) });
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async assignTask(projectId, teamId, taskFilePath, taskName, targetRole, teamMemberId, sessionName) {
|
|
113
|
+
const data = await this.loadTaskData();
|
|
114
|
+
const task = {
|
|
115
|
+
id: uuidv4(),
|
|
116
|
+
projectId,
|
|
117
|
+
teamId,
|
|
118
|
+
taskFilePath,
|
|
119
|
+
taskName,
|
|
120
|
+
targetRole,
|
|
121
|
+
assignedTeamMemberId: teamMemberId,
|
|
122
|
+
assignedSessionName: sessionName,
|
|
123
|
+
assignedAt: new Date().toISOString(),
|
|
124
|
+
status: 'assigned'
|
|
125
|
+
};
|
|
126
|
+
data.tasks.push(task);
|
|
127
|
+
await this.saveTaskData(data);
|
|
128
|
+
// Emit task assigned event
|
|
129
|
+
this.emit('task_assigned', task);
|
|
130
|
+
return task;
|
|
131
|
+
}
|
|
132
|
+
async updateTaskStatus(taskId, status, blockReason, requestorOrgRole) {
|
|
133
|
+
const data = await this.loadTaskData();
|
|
134
|
+
const task = data.tasks.find(t => t.id === taskId);
|
|
135
|
+
if (!task) {
|
|
136
|
+
throw new Error(`Task with ID ${taskId} not found`);
|
|
137
|
+
}
|
|
138
|
+
// Validate transition if requestor role is provided
|
|
139
|
+
if (requestorOrgRole) {
|
|
140
|
+
const validation = this.validateStatusTransition(task.status, status, requestorOrgRole);
|
|
141
|
+
if (!validation.valid) {
|
|
142
|
+
throw new Error(`Status transition rejected: ${validation.reason}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const previousStatus = task.status;
|
|
146
|
+
task.status = status;
|
|
147
|
+
task.lastCheckedAt = new Date().toISOString();
|
|
148
|
+
if (status === 'blocked' && blockReason) {
|
|
149
|
+
task.blockReason = blockReason;
|
|
150
|
+
}
|
|
151
|
+
// Clear working notes on terminal statuses — not long-term memory
|
|
152
|
+
if (status === 'completed' || status === 'verified') {
|
|
153
|
+
task.workingNotes = undefined;
|
|
154
|
+
task.workingNotesUpdatedAt = undefined;
|
|
155
|
+
}
|
|
156
|
+
await this.saveTaskData(data);
|
|
157
|
+
// Emit task completed event if status is completed
|
|
158
|
+
if (status === 'completed') {
|
|
159
|
+
this.emit('task_completed', task);
|
|
160
|
+
}
|
|
161
|
+
// Architecture Upgrade Phase 6: emit task workflow events for action trigger chain.
|
|
162
|
+
// These events drive the wake chain: done → TL verifies → verified → orc notified.
|
|
163
|
+
this.emitTaskWorkflowEvent(task, previousStatus, status, data);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Emit task workflow events that drive the action trigger chain.
|
|
167
|
+
* Only emits for transitions that require a different actor to wake up.
|
|
168
|
+
*
|
|
169
|
+
* @param task - The updated task
|
|
170
|
+
* @param fromStatus - Previous status
|
|
171
|
+
* @param toStatus - New status
|
|
172
|
+
* @param data - Current task tracking data (for team:all_tasks_done check)
|
|
173
|
+
*/
|
|
174
|
+
emitTaskWorkflowEvent(task, fromStatus, toStatus, data) {
|
|
175
|
+
// Map status transitions to event types
|
|
176
|
+
const eventMap = {
|
|
177
|
+
'assigned': 'task:assigned',
|
|
178
|
+
'done': 'task:done',
|
|
179
|
+
'verified': 'task:verified',
|
|
180
|
+
'blocked': 'task:blocked',
|
|
181
|
+
'failed': 'task:failed',
|
|
182
|
+
'needs_clarification': 'task:needs_clarification',
|
|
183
|
+
};
|
|
184
|
+
const eventType = eventMap[toStatus];
|
|
185
|
+
if (!eventType)
|
|
186
|
+
return;
|
|
187
|
+
// Don't re-emit if status didn't actually change
|
|
188
|
+
if (fromStatus === toStatus)
|
|
189
|
+
return;
|
|
190
|
+
const payload = {
|
|
191
|
+
taskId: task.id,
|
|
192
|
+
taskName: task.taskName,
|
|
193
|
+
taskStatus: toStatus,
|
|
194
|
+
assignedSessionName: task.assignedSessionName,
|
|
195
|
+
ownerMemberId: task.assignedTeamMemberId,
|
|
196
|
+
teamId: task.teamId,
|
|
197
|
+
parentTaskId: task.parentTaskId,
|
|
198
|
+
delegatedBySession: task.delegatedBySession,
|
|
199
|
+
};
|
|
200
|
+
this.logger.info('Task workflow event emitted', { eventType, ...payload });
|
|
201
|
+
this.emit('task_workflow_event', { type: eventType, ...payload });
|
|
202
|
+
// Check for team:all_tasks_done (with active execution task filter)
|
|
203
|
+
if (toStatus === 'verified' || toStatus === 'completed') {
|
|
204
|
+
const activeStatuses = [
|
|
205
|
+
'assigned', 'accepted', 'active', 'working', 'blocked', 'done', 'verifying', 'submitted',
|
|
206
|
+
];
|
|
207
|
+
const activeTeamTasks = data.tasks.filter(t => t.teamId === task.teamId && activeStatuses.includes(t.status));
|
|
208
|
+
if (activeTeamTasks.length === 0) {
|
|
209
|
+
this.logger.info('All team tasks done — emitting team:all_tasks_done', { teamId: task.teamId });
|
|
210
|
+
this.emit('task_workflow_event', {
|
|
211
|
+
type: 'team:all_tasks_done',
|
|
212
|
+
teamId: task.teamId,
|
|
213
|
+
taskId: task.id,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Accept a task — transitions from 'assigned' to 'accepted'.
|
|
220
|
+
* Records the agent's structured understanding of the task.
|
|
221
|
+
*
|
|
222
|
+
* @param taskId - Task ID to accept
|
|
223
|
+
* @param sessionName - Session name of the accepting agent (must match assignee)
|
|
224
|
+
* @param understanding - Agent's structured understanding of the task scope
|
|
225
|
+
* @returns The updated task
|
|
226
|
+
* @throws If task not found, not in 'assigned' status, or session doesn't match
|
|
227
|
+
*/
|
|
228
|
+
async acceptTask(taskId, sessionName, understanding) {
|
|
229
|
+
const data = await this.loadTaskData();
|
|
230
|
+
const task = data.tasks.find(t => t.id === taskId);
|
|
231
|
+
if (!task) {
|
|
232
|
+
throw new Error(`Task with ID ${taskId} not found`);
|
|
233
|
+
}
|
|
234
|
+
if (task.status !== 'assigned') {
|
|
235
|
+
throw new Error(`Task ${taskId} is in '${task.status}' status, expected 'assigned'`);
|
|
236
|
+
}
|
|
237
|
+
if (task.assignedSessionName !== sessionName) {
|
|
238
|
+
throw new Error(`Session '${sessionName}' is not the assignee of task ${taskId}`);
|
|
239
|
+
}
|
|
240
|
+
task.status = 'accepted';
|
|
241
|
+
task.acceptedAt = new Date().toISOString();
|
|
242
|
+
task.acceptanceNote = understanding;
|
|
243
|
+
task.lastCheckedAt = new Date().toISOString();
|
|
244
|
+
await this.saveTaskData(data);
|
|
245
|
+
this.emit('task_accepted', task);
|
|
246
|
+
return task;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Request clarification on a task — transitions to 'needs_clarification'.
|
|
250
|
+
* Signals that the agent needs more info before starting work.
|
|
251
|
+
*
|
|
252
|
+
* @param taskId - Task ID to request clarification for
|
|
253
|
+
* @param sessionName - Session name of the requesting agent (must match assignee)
|
|
254
|
+
* @param question - What the agent needs clarified
|
|
255
|
+
* @returns The updated task
|
|
256
|
+
* @throws If task not found, not in valid status, or session doesn't match
|
|
257
|
+
*/
|
|
258
|
+
async requestClarification(taskId, sessionName, question) {
|
|
259
|
+
const data = await this.loadTaskData();
|
|
260
|
+
const task = data.tasks.find(t => t.id === taskId);
|
|
261
|
+
if (!task) {
|
|
262
|
+
throw new Error(`Task with ID ${taskId} not found`);
|
|
263
|
+
}
|
|
264
|
+
if (task.status !== 'assigned' && task.status !== 'accepted') {
|
|
265
|
+
throw new Error(`Task ${taskId} is in '${task.status}' status, expected 'assigned' or 'accepted'`);
|
|
266
|
+
}
|
|
267
|
+
if (task.assignedSessionName !== sessionName) {
|
|
268
|
+
throw new Error(`Session '${sessionName}' is not the assignee of task ${taskId}`);
|
|
269
|
+
}
|
|
270
|
+
task.status = 'needs_clarification';
|
|
271
|
+
task.clarificationRequest = question;
|
|
272
|
+
task.lastCheckedAt = new Date().toISOString();
|
|
273
|
+
await this.saveTaskData(data);
|
|
274
|
+
this.emit('task_needs_clarification', task);
|
|
275
|
+
return task;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Save working notes for a task — persists agent's current working state
|
|
279
|
+
* across session restarts. Not long-term memory; cleared on task completion.
|
|
280
|
+
*
|
|
281
|
+
* @param taskId - Task ID
|
|
282
|
+
* @param sessionName - Session name (must match assignee)
|
|
283
|
+
* @param notes - Working notes content
|
|
284
|
+
* @throws If task not found or session doesn't match assignee
|
|
285
|
+
*/
|
|
286
|
+
async updateWorkingNotes(taskId, sessionName, notes) {
|
|
287
|
+
const data = await this.loadTaskData();
|
|
288
|
+
const task = data.tasks.find(t => t.id === taskId);
|
|
289
|
+
if (!task)
|
|
290
|
+
throw new Error(`Task with ID ${taskId} not found`);
|
|
291
|
+
if (task.assignedSessionName !== sessionName) {
|
|
292
|
+
throw new Error(`Session '${sessionName}' is not the assignee of task ${taskId}`);
|
|
293
|
+
}
|
|
294
|
+
task.workingNotes = notes;
|
|
295
|
+
task.workingNotesUpdatedAt = new Date().toISOString();
|
|
296
|
+
await this.saveTaskData(data);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Validate that a status transition is allowed based on the Task State Ownership Matrix.
|
|
300
|
+
* Enforces who can write which status and which transitions are legal.
|
|
301
|
+
*
|
|
302
|
+
* @param currentStatus - Current task status
|
|
303
|
+
* @param newStatus - Requested new status
|
|
304
|
+
* @param requestorOrgRole - Organizational role of the requestor
|
|
305
|
+
* @returns Validation result with optional reason for rejection
|
|
306
|
+
*/
|
|
307
|
+
validateStatusTransition(currentStatus, newStatus, requestorOrgRole) {
|
|
308
|
+
// Status ownership matrix: who can write which status
|
|
309
|
+
const executorStatuses = ['accepted', 'needs_clarification', 'active', 'blocked', 'done', 'working', 'submitted'];
|
|
310
|
+
const tlStatuses = ['verifying', 'verified', 'failed', 'cancelled', 'assigned'];
|
|
311
|
+
const orcStatuses = ['cancelled', 'assigned', 'ready', 'backlog', 'pending_assignment'];
|
|
312
|
+
// Check ownership
|
|
313
|
+
if (requestorOrgRole === 'executor') {
|
|
314
|
+
if (!executorStatuses.includes(newStatus)) {
|
|
315
|
+
return { valid: false, reason: `Executor cannot set status to '${newStatus}'. Allowed: ${executorStatuses.join(', ')}` };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else if (requestorOrgRole === 'team-lead') {
|
|
319
|
+
if (!tlStatuses.includes(newStatus) && !executorStatuses.includes(newStatus)) {
|
|
320
|
+
return { valid: false, reason: `Team Lead cannot set status to '${newStatus}'` };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else if (requestorOrgRole === 'orchestrator') {
|
|
324
|
+
if (!orcStatuses.includes(newStatus)) {
|
|
325
|
+
return { valid: false, reason: `Orchestrator cannot set status to '${newStatus}'. Allowed: ${orcStatuses.join(', ')}` };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Illegal transitions
|
|
329
|
+
if (currentStatus === 'assigned' && newStatus === 'verified') {
|
|
330
|
+
return { valid: false, reason: 'Cannot skip execution: assigned → verified is illegal' };
|
|
331
|
+
}
|
|
332
|
+
if (currentStatus === 'done' && newStatus === 'assigned') {
|
|
333
|
+
return { valid: false, reason: 'Cannot reassign directly from done. Must go through failed first.' };
|
|
334
|
+
}
|
|
335
|
+
return { valid: true };
|
|
336
|
+
}
|
|
337
|
+
async removeTask(taskId) {
|
|
338
|
+
const data = await this.loadTaskData();
|
|
339
|
+
data.tasks = data.tasks.filter(t => t.id !== taskId);
|
|
340
|
+
await this.saveTaskData(data);
|
|
341
|
+
}
|
|
342
|
+
async addTaskToQueue(taskInfo) {
|
|
343
|
+
const data = await this.loadTaskData();
|
|
344
|
+
const task = {
|
|
345
|
+
id: uuidv4(),
|
|
346
|
+
projectId: taskInfo.projectId,
|
|
347
|
+
teamId: taskInfo.teamId,
|
|
348
|
+
taskFilePath: taskInfo.taskFilePath,
|
|
349
|
+
taskName: taskInfo.taskName,
|
|
350
|
+
targetRole: taskInfo.targetRole,
|
|
351
|
+
assignedTeamMemberId: 'orchestrator', // Queued for orchestrator assignment
|
|
352
|
+
assignedSessionName: CREWLY_CONSTANTS.SESSIONS.ORCHESTRATOR_NAME,
|
|
353
|
+
assignedAt: taskInfo.createdAt,
|
|
354
|
+
status: 'pending_assignment', // New status for tasks awaiting assignment
|
|
355
|
+
priority: taskInfo.priority
|
|
356
|
+
};
|
|
357
|
+
data.tasks.push(task);
|
|
358
|
+
await this.saveTaskData(data);
|
|
359
|
+
return task;
|
|
360
|
+
}
|
|
361
|
+
async getTasksForProject(projectId) {
|
|
362
|
+
const data = await this.loadTaskData();
|
|
363
|
+
return data.tasks.filter(t => t.projectId === projectId);
|
|
364
|
+
}
|
|
365
|
+
async getTasksForTeamMember(teamMemberId) {
|
|
366
|
+
const data = await this.loadTaskData();
|
|
367
|
+
return data.tasks.filter(t => t.assignedTeamMemberId === teamMemberId);
|
|
368
|
+
}
|
|
369
|
+
async getAllInProgressTasks() {
|
|
370
|
+
const data = await this.loadTaskData();
|
|
371
|
+
return data.tasks;
|
|
372
|
+
}
|
|
373
|
+
// Utility method to scan project tasks and sync with file system
|
|
374
|
+
async syncTasksWithFileSystem(projectPath, projectId) {
|
|
375
|
+
const tasksPath = path.join(projectPath, '.crewly', 'tasks');
|
|
376
|
+
if (!fsSync.existsSync(tasksPath)) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
const data = await this.loadTaskData();
|
|
380
|
+
const projectTasks = data.tasks.filter(t => t.projectId === projectId);
|
|
381
|
+
// Check if assigned tasks still exist in in_progress folder
|
|
382
|
+
for (const task of projectTasks) {
|
|
383
|
+
const expectedInProgressPath = task.taskFilePath.replace('/open/', '/in_progress/');
|
|
384
|
+
const taskStillInProgress = fsSync.existsSync(expectedInProgressPath);
|
|
385
|
+
if (!taskStillInProgress) {
|
|
386
|
+
// Task was moved manually, check where it went
|
|
387
|
+
const baseName = path.basename(task.taskFilePath);
|
|
388
|
+
const milestoneDir = path.dirname(path.dirname(task.taskFilePath));
|
|
389
|
+
const doneFile = path.join(milestoneDir, 'done', baseName);
|
|
390
|
+
const blockedFile = path.join(milestoneDir, 'blocked', baseName);
|
|
391
|
+
if (fsSync.existsSync(doneFile)) {
|
|
392
|
+
// Task was completed, remove from tracking
|
|
393
|
+
await this.removeTask(task.id);
|
|
394
|
+
}
|
|
395
|
+
else if (fsSync.existsSync(blockedFile)) {
|
|
396
|
+
// Task was blocked, update status
|
|
397
|
+
await this.updateTaskStatus(task.id, 'blocked', 'Moved to blocked folder manually');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Get available open tasks for a project
|
|
403
|
+
async getOpenTasks(projectPath) {
|
|
404
|
+
const tasksPath = path.join(projectPath, '.crewly', 'tasks');
|
|
405
|
+
const openTasks = [];
|
|
406
|
+
if (!fsSync.existsSync(tasksPath)) {
|
|
407
|
+
return openTasks;
|
|
408
|
+
}
|
|
409
|
+
const milestones = await fs.readdir(tasksPath);
|
|
410
|
+
for (const milestone of milestones) {
|
|
411
|
+
if (!milestone.startsWith('m') || !milestone.includes('_'))
|
|
412
|
+
continue;
|
|
413
|
+
const milestonePath = path.join(tasksPath, milestone);
|
|
414
|
+
const openFolderPath = path.join(milestonePath, 'open');
|
|
415
|
+
if (fsSync.existsSync(openFolderPath)) {
|
|
416
|
+
const openFiles = await fs.readdir(openFolderPath);
|
|
417
|
+
for (const file of openFiles) {
|
|
418
|
+
if (file.endsWith('.md')) {
|
|
419
|
+
const fullPath = path.join(openFolderPath, file);
|
|
420
|
+
// Parse role from filename (assumes format: NN_task_name_ROLE.md)
|
|
421
|
+
const roleMatch = file.match(/_([a-z]+)\.md$/);
|
|
422
|
+
const targetRole = roleMatch ? roleMatch[1] : 'unknown';
|
|
423
|
+
openTasks.push({
|
|
424
|
+
filePath: fullPath,
|
|
425
|
+
fileName: file,
|
|
426
|
+
taskName: this.extractTaskNameFromFile(file),
|
|
427
|
+
targetRole,
|
|
428
|
+
milestoneFolder: milestone,
|
|
429
|
+
statusFolder: 'open'
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return openTasks;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Store monitoring IDs (schedules and subscriptions) for a task so they can be
|
|
439
|
+
* automatically cleaned up when the task completes.
|
|
440
|
+
*
|
|
441
|
+
* @param taskId - The task ID to associate monitoring with
|
|
442
|
+
* @param scheduleIds - Array of schedule check IDs to link
|
|
443
|
+
* @param subscriptionIds - Array of event subscription IDs to link
|
|
444
|
+
*/
|
|
445
|
+
async addMonitoringIds(taskId, scheduleIds, subscriptionIds) {
|
|
446
|
+
const data = await this.loadTaskData();
|
|
447
|
+
const task = data.tasks.find(t => t.id === taskId);
|
|
448
|
+
if (!task) {
|
|
449
|
+
throw new Error(`Task with ID ${taskId} not found`);
|
|
450
|
+
}
|
|
451
|
+
task.scheduleIds = [...(task.scheduleIds || []), ...scheduleIds];
|
|
452
|
+
task.subscriptionIds = [...(task.subscriptionIds || []), ...subscriptionIds];
|
|
453
|
+
await this.saveTaskData(data);
|
|
454
|
+
this.logger.info('Added monitoring IDs to task', {
|
|
455
|
+
taskId,
|
|
456
|
+
scheduleIds,
|
|
457
|
+
subscriptionIds,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Find all tasks assigned to a specific agent session.
|
|
462
|
+
*
|
|
463
|
+
* @param sessionName - The agent session name
|
|
464
|
+
* @returns Array of tasks assigned to the session
|
|
465
|
+
*/
|
|
466
|
+
async getTasksBySessionName(sessionName) {
|
|
467
|
+
const data = await this.loadTaskData();
|
|
468
|
+
return data.tasks.filter(t => t.assignedSessionName === sessionName);
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Collect all monitoring IDs (schedules and subscriptions) for tasks assigned
|
|
472
|
+
* to a specific agent session. Used for auto-cleanup when tasks complete.
|
|
473
|
+
*
|
|
474
|
+
* @param sessionName - The agent session name
|
|
475
|
+
* @returns Object with arrays of scheduleIds and subscriptionIds
|
|
476
|
+
*/
|
|
477
|
+
async getMonitoringIdsForSession(sessionName) {
|
|
478
|
+
const tasks = await this.getTasksBySessionName(sessionName);
|
|
479
|
+
const scheduleIds = [];
|
|
480
|
+
const subscriptionIds = [];
|
|
481
|
+
for (const task of tasks) {
|
|
482
|
+
if (task.scheduleIds) {
|
|
483
|
+
scheduleIds.push(...task.scheduleIds);
|
|
484
|
+
}
|
|
485
|
+
if (task.subscriptionIds) {
|
|
486
|
+
subscriptionIds.push(...task.subscriptionIds);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return { scheduleIds, subscriptionIds };
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Detect orphan tasks: tasks stuck in in_progress/assigned/active with no
|
|
493
|
+
* corresponding active agent. Returns the list without modifying state.
|
|
494
|
+
*
|
|
495
|
+
* @param getTeamStatus - Function returning current team configurations
|
|
496
|
+
* @param staleThresholdMs - Age threshold in ms to consider a task stale (default: 24 hours)
|
|
497
|
+
* @returns Array of orphan tasks with staleness info
|
|
498
|
+
*/
|
|
499
|
+
async detectOrphanTasks(getTeamStatus, staleThresholdMs = 24 * 60 * 60 * 1000) {
|
|
500
|
+
const data = await this.loadTaskData();
|
|
501
|
+
const activeTasks = data.tasks.filter(t => t.status === 'assigned' || t.status === 'active' || t.status === 'working');
|
|
502
|
+
if (activeTasks.length === 0)
|
|
503
|
+
return [];
|
|
504
|
+
// Build set of all known agent session names + member IDs
|
|
505
|
+
const teams = await getTeamStatus();
|
|
506
|
+
const knownAgents = new Set();
|
|
507
|
+
for (const team of teams) {
|
|
508
|
+
for (const member of (team.members || [])) {
|
|
509
|
+
if (member.sessionName)
|
|
510
|
+
knownAgents.add(member.sessionName);
|
|
511
|
+
if (member.id)
|
|
512
|
+
knownAgents.add(member.id);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const now = Date.now();
|
|
516
|
+
const orphans = [];
|
|
517
|
+
for (const task of activeTasks) {
|
|
518
|
+
const agentKnown = knownAgents.has(task.assignedSessionName) ||
|
|
519
|
+
knownAgents.has(task.assignedTeamMemberId);
|
|
520
|
+
const assignedTime = new Date(task.assignedAt).getTime();
|
|
521
|
+
const staleSinceMs = now - assignedTime;
|
|
522
|
+
// Orphan if agent doesn't exist in any team OR task is stale
|
|
523
|
+
if (!agentKnown || staleSinceMs > staleThresholdMs) {
|
|
524
|
+
orphans.push({ ...task, staleSinceMs });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return orphans;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Bulk cleanup orphan tasks by marking them as cancelled and optionally
|
|
531
|
+
* moving their files back to the open folder.
|
|
532
|
+
*
|
|
533
|
+
* @param taskIds - Array of task IDs to clean up
|
|
534
|
+
* @param action - 'cancel' marks tasks as cancelled, 'reopen' moves them back to open
|
|
535
|
+
* @returns Cleanup report with counts
|
|
536
|
+
*/
|
|
537
|
+
async cleanupOrphanTasks(taskIds, action = 'cancel') {
|
|
538
|
+
const report = { cleaned: 0, errors: [] };
|
|
539
|
+
const data = await this.loadTaskData();
|
|
540
|
+
for (const taskId of taskIds) {
|
|
541
|
+
const task = data.tasks.find(t => t.id === taskId);
|
|
542
|
+
if (!task) {
|
|
543
|
+
report.errors.push(`Task ${taskId} not found`);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
if (action === 'reopen') {
|
|
548
|
+
const moved = await this.moveTaskBackToOpen(task);
|
|
549
|
+
if (moved) {
|
|
550
|
+
await this.removeTask(taskId);
|
|
551
|
+
report.cleaned++;
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
// File may already be gone — just remove from tracking
|
|
555
|
+
await this.removeTask(taskId);
|
|
556
|
+
report.cleaned++;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
// Cancel: update status and remove from active tracking
|
|
561
|
+
task.status = 'cancelled';
|
|
562
|
+
task.completedAt = new Date().toISOString();
|
|
563
|
+
await this.saveTaskData(data);
|
|
564
|
+
await this.removeTask(taskId);
|
|
565
|
+
report.cleaned++;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
report.errors.push(`Failed to clean task ${taskId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
this.logger.info('Orphan task cleanup complete', {
|
|
573
|
+
action,
|
|
574
|
+
cleaned: report.cleaned,
|
|
575
|
+
errors: report.errors.length,
|
|
576
|
+
});
|
|
577
|
+
return report;
|
|
578
|
+
}
|
|
579
|
+
extractTaskNameFromFile(filename) {
|
|
580
|
+
// Remove extension and number prefix
|
|
581
|
+
return filename
|
|
582
|
+
.replace('.md', '')
|
|
583
|
+
.replace(/^\d+_/, '')
|
|
584
|
+
.replace(/_[a-z]+$/, '') // Remove role suffix
|
|
585
|
+
.replace(/_/g, ' ')
|
|
586
|
+
.replace(/\b\w/g, l => l.toUpperCase());
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Recovers abandoned in-progress tasks by checking agent status and moving inactive tasks back to open
|
|
590
|
+
* @param getTeamStatus Function to get current team status from API
|
|
591
|
+
* @returns Recovery report with actions taken
|
|
592
|
+
*/
|
|
593
|
+
async recoverAbandonedTasks(getTeamStatus) {
|
|
594
|
+
const report = {
|
|
595
|
+
totalInProgress: 0,
|
|
596
|
+
recovered: 0,
|
|
597
|
+
skipped: 0,
|
|
598
|
+
errors: [],
|
|
599
|
+
recoveredTasks: []
|
|
600
|
+
};
|
|
601
|
+
try {
|
|
602
|
+
this.logger.info('Starting task recovery check');
|
|
603
|
+
const data = await this.loadTaskData();
|
|
604
|
+
const inProgressTasks = data.tasks.filter(t => t.status === 'assigned' || t.status === 'active');
|
|
605
|
+
report.totalInProgress = inProgressTasks.length;
|
|
606
|
+
if (inProgressTasks.length === 0) {
|
|
607
|
+
this.logger.info('No in-progress tasks found to recover');
|
|
608
|
+
return report;
|
|
609
|
+
}
|
|
610
|
+
this.logger.info('Found in-progress tasks to check', { count: inProgressTasks.length });
|
|
611
|
+
// Get current team status
|
|
612
|
+
const teams = await getTeamStatus();
|
|
613
|
+
const activeMembers = new Set();
|
|
614
|
+
teams.forEach(team => {
|
|
615
|
+
team.members?.forEach((member) => {
|
|
616
|
+
if (member.agentStatus === 'active' && member.workingStatus === 'in_progress') {
|
|
617
|
+
activeMembers.add(member.sessionName);
|
|
618
|
+
activeMembers.add(member.id);
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
this.logger.info('Found active working members', { count: activeMembers.size });
|
|
623
|
+
// Check each in-progress task
|
|
624
|
+
for (const task of inProgressTasks) {
|
|
625
|
+
try {
|
|
626
|
+
const isAgentActive = activeMembers.has(task.assignedSessionName) ||
|
|
627
|
+
activeMembers.has(task.assignedTeamMemberId);
|
|
628
|
+
if (isAgentActive) {
|
|
629
|
+
this.logger.info('Agent is still active, keeping task', { sessionName: task.assignedSessionName, taskName: task.taskName });
|
|
630
|
+
report.skipped++;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
this.logger.warn('Agent is inactive, recovering task', { sessionName: task.assignedSessionName, taskName: task.taskName });
|
|
634
|
+
// Move task back to open folder and clean metadata
|
|
635
|
+
const recovered = await this.moveTaskBackToOpen(task);
|
|
636
|
+
if (recovered) {
|
|
637
|
+
// Remove from JSON tracking
|
|
638
|
+
await this.removeTask(task.id);
|
|
639
|
+
report.recovered++;
|
|
640
|
+
report.recoveredTasks.push(task.taskName);
|
|
641
|
+
this.logger.info('Successfully recovered task', { taskName: task.taskName });
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
report.errors.push(`Failed to move task back to open: ${task.taskName}`);
|
|
645
|
+
this.logger.error('Failed to recover task', { taskName: task.taskName });
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
const errorMsg = `Error processing task ${task.taskName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
650
|
+
report.errors.push(errorMsg);
|
|
651
|
+
this.logger.error('Error processing task during recovery', { taskName: task.taskName, error: error instanceof Error ? error.message : String(error) });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
this.logger.info('Recovery complete', { recovered: report.recovered, skipped: report.skipped, errors: report.errors.length });
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
const errorMsg = `Task recovery failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
658
|
+
report.errors.push(errorMsg);
|
|
659
|
+
this.logger.error('Task recovery failed', { error: error instanceof Error ? error.message : String(error) });
|
|
660
|
+
}
|
|
661
|
+
return report;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Moves a task back to the open folder and cleans assignment metadata
|
|
665
|
+
*/
|
|
666
|
+
async moveTaskBackToOpen(task) {
|
|
667
|
+
try {
|
|
668
|
+
// Check if task file still exists in in_progress
|
|
669
|
+
if (!fsSync.existsSync(task.taskFilePath)) {
|
|
670
|
+
this.logger.warn('Task file not found in in_progress', { taskFilePath: task.taskFilePath });
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
// Read current task content
|
|
674
|
+
const content = await fs.readFile(task.taskFilePath, 'utf-8');
|
|
675
|
+
// Clean assignment metadata
|
|
676
|
+
const cleanedContent = this.cleanAssignmentMetadata(content);
|
|
677
|
+
// Calculate target path in open folder
|
|
678
|
+
const openPath = task.taskFilePath.replace('/in_progress/', '/open/');
|
|
679
|
+
const openDir = path.dirname(openPath);
|
|
680
|
+
// Ensure open directory exists
|
|
681
|
+
if (!fsSync.existsSync(openDir)) {
|
|
682
|
+
await fs.mkdir(openDir, { recursive: true });
|
|
683
|
+
}
|
|
684
|
+
// Write cleaned content to open folder
|
|
685
|
+
await fs.writeFile(openPath, cleanedContent, 'utf-8');
|
|
686
|
+
// Remove from in_progress folder
|
|
687
|
+
await fs.unlink(task.taskFilePath);
|
|
688
|
+
this.logger.info('Moved task back to open', { from: task.taskFilePath, to: openPath });
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
this.logger.error('Failed to move task back to open', { error: error instanceof Error ? error.message : String(error) });
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Removes assignment metadata sections from task content
|
|
698
|
+
*/
|
|
699
|
+
cleanAssignmentMetadata(content) {
|
|
700
|
+
// Remove ## Assignment Information section and everything after it
|
|
701
|
+
// This preserves the original task content but removes assignment metadata
|
|
702
|
+
const lines = content.split('\n');
|
|
703
|
+
const cleanedLines = [];
|
|
704
|
+
let inAssignmentSection = false;
|
|
705
|
+
for (const line of lines) {
|
|
706
|
+
if (line.trim().startsWith('## Assignment Information')) {
|
|
707
|
+
inAssignmentSection = true;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
// If we hit another ## section after assignment, stop skipping
|
|
711
|
+
if (inAssignmentSection && line.trim().startsWith('## ') && !line.includes('Assignment Information')) {
|
|
712
|
+
inAssignmentSection = false;
|
|
713
|
+
}
|
|
714
|
+
if (!inAssignmentSection) {
|
|
715
|
+
cleanedLines.push(line);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
// Remove trailing empty lines
|
|
719
|
+
while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1].trim() === '') {
|
|
720
|
+
cleanedLines.pop();
|
|
721
|
+
}
|
|
722
|
+
return cleanedLines.join('\n');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
//# sourceMappingURL=task-tracking.service.js.map
|