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.
Files changed (189) hide show
  1. package/config/domain-sops/EXAMPLE.sop.md +21 -0
  2. package/config/overlays/can-decide.md +5 -0
  3. package/config/overlays/can-delegate.md +5 -0
  4. package/config/overlays/can-user-reply.md +5 -0
  5. package/config/overlays/can-verify.md +5 -0
  6. package/config/risk-policies/EXAMPLE.policy.md +22 -0
  7. package/config/roles/developer/fragments/role-boundary.md +30 -0
  8. package/config/roles/orchestrator/fragments/role-boundary.md +25 -0
  9. package/config/roles/orchestrator/prompt.md +40 -6
  10. package/config/roles/team-leader/fragments/role-boundary.md +22 -0
  11. package/config/skills/agent/core/get-my-tasks/execute.sh +16 -0
  12. package/config/skills/agent/core/save-working-state/execute.sh +19 -0
  13. package/config/skills/agent/core/update-user-profile/execute.sh +13 -0
  14. package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts +34 -0
  15. package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
  16. package/dist/backend/backend/src/controllers/chat/chat.controller.js +107 -0
  17. package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
  18. package/dist/backend/backend/src/controllers/chat/chat.routes.d.ts.map +1 -1
  19. package/dist/backend/backend/src/controllers/chat/chat.routes.js +5 -1
  20. package/dist/backend/backend/src/controllers/chat/chat.routes.js.map +1 -1
  21. package/dist/backend/backend/src/controllers/chat/index.d.ts +1 -1
  22. package/dist/backend/backend/src/controllers/chat/index.d.ts.map +1 -1
  23. package/dist/backend/backend/src/controllers/chat/index.js +1 -1
  24. package/dist/backend/backend/src/controllers/chat/index.js.map +1 -1
  25. package/dist/backend/backend/src/controllers/memory/index.d.ts +1 -1
  26. package/dist/backend/backend/src/controllers/memory/index.d.ts.map +1 -1
  27. package/dist/backend/backend/src/controllers/memory/index.js +1 -1
  28. package/dist/backend/backend/src/controllers/memory/index.js.map +1 -1
  29. package/dist/backend/backend/src/controllers/memory/memory.controller.d.ts +35 -0
  30. package/dist/backend/backend/src/controllers/memory/memory.controller.d.ts.map +1 -1
  31. package/dist/backend/backend/src/controllers/memory/memory.controller.js +61 -1
  32. package/dist/backend/backend/src/controllers/memory/memory.controller.js.map +1 -1
  33. package/dist/backend/backend/src/controllers/memory/memory.routes.d.ts +2 -0
  34. package/dist/backend/backend/src/controllers/memory/memory.routes.d.ts.map +1 -1
  35. package/dist/backend/backend/src/controllers/memory/memory.routes.js +5 -1
  36. package/dist/backend/backend/src/controllers/memory/memory.routes.js.map +1 -1
  37. package/dist/backend/backend/src/controllers/system/cron-task.controller.d.ts +1 -0
  38. package/dist/backend/backend/src/controllers/system/cron-task.controller.d.ts.map +1 -1
  39. package/dist/backend/backend/src/controllers/system/cron-task.controller.js +12 -6
  40. package/dist/backend/backend/src/controllers/system/cron-task.controller.js.map +1 -1
  41. package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts +24 -0
  42. package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +1 -1
  43. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +107 -0
  44. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +1 -1
  45. package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
  46. package/dist/backend/backend/src/controllers/team/team.controller.js +49 -0
  47. package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
  48. package/dist/backend/backend/src/index.d.ts.map +1 -1
  49. package/dist/backend/backend/src/index.js +46 -42
  50. package/dist/backend/backend/src/index.js.map +1 -1
  51. package/dist/backend/backend/src/routes/modules/task-management.routes.d.ts.map +1 -1
  52. package/dist/backend/backend/src/routes/modules/task-management.routes.js +5 -0
  53. package/dist/backend/backend/src/routes/modules/task-management.routes.js.map +1 -1
  54. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +24 -0
  55. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  56. package/dist/backend/backend/src/services/agent/agent-registration.service.js +104 -2
  57. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  58. package/dist/backend/backend/src/services/ai/prompt-modules/capability-overlay.module.d.ts +33 -0
  59. package/dist/backend/backend/src/services/ai/prompt-modules/capability-overlay.module.d.ts.map +1 -0
  60. package/dist/backend/backend/src/services/ai/prompt-modules/capability-overlay.module.js +70 -0
  61. package/dist/backend/backend/src/services/ai/prompt-modules/capability-overlay.module.js.map +1 -0
  62. package/dist/backend/backend/src/services/ai/prompt-modules/domain-sop.module.d.ts +35 -0
  63. package/dist/backend/backend/src/services/ai/prompt-modules/domain-sop.module.d.ts.map +1 -0
  64. package/dist/backend/backend/src/services/ai/prompt-modules/domain-sop.module.js +50 -0
  65. package/dist/backend/backend/src/services/ai/prompt-modules/domain-sop.module.js.map +1 -0
  66. package/dist/backend/backend/src/services/ai/prompt-modules/index.d.ts +1 -0
  67. package/dist/backend/backend/src/services/ai/prompt-modules/index.d.ts.map +1 -1
  68. package/dist/backend/backend/src/services/ai/prompt-modules/index.js +1 -0
  69. package/dist/backend/backend/src/services/ai/prompt-modules/index.js.map +1 -1
  70. package/dist/backend/backend/src/services/ai/prompt-modules/markdown-file-module.d.ts +46 -0
  71. package/dist/backend/backend/src/services/ai/prompt-modules/markdown-file-module.d.ts.map +1 -0
  72. package/dist/backend/backend/src/services/ai/prompt-modules/markdown-file-module.js +44 -0
  73. package/dist/backend/backend/src/services/ai/prompt-modules/markdown-file-module.js.map +1 -0
  74. package/dist/backend/backend/src/services/ai/prompt-modules/prompt-assembly.service.d.ts.map +1 -1
  75. package/dist/backend/backend/src/services/ai/prompt-modules/prompt-assembly.service.js +13 -3
  76. package/dist/backend/backend/src/services/ai/prompt-modules/prompt-assembly.service.js.map +1 -1
  77. package/dist/backend/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts +22 -0
  78. package/dist/backend/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/ai/prompt-modules/prompt-module.interface.js +2 -11
  80. package/dist/backend/backend/src/services/ai/prompt-modules/prompt-module.interface.js.map +1 -1
  81. package/dist/backend/backend/src/services/ai/prompt-modules/recovery.module.d.ts.map +1 -1
  82. package/dist/backend/backend/src/services/ai/prompt-modules/recovery.module.js +10 -2
  83. package/dist/backend/backend/src/services/ai/prompt-modules/recovery.module.js.map +1 -1
  84. package/dist/backend/backend/src/services/ai/prompt-modules/risk-policy.module.d.ts +35 -0
  85. package/dist/backend/backend/src/services/ai/prompt-modules/risk-policy.module.d.ts.map +1 -0
  86. package/dist/backend/backend/src/services/ai/prompt-modules/risk-policy.module.js +50 -0
  87. package/dist/backend/backend/src/services/ai/prompt-modules/risk-policy.module.js.map +1 -0
  88. package/dist/backend/backend/src/services/ai/prompt-modules/role-boundary.module.d.ts +67 -0
  89. package/dist/backend/backend/src/services/ai/prompt-modules/role-boundary.module.d.ts.map +1 -0
  90. package/dist/backend/backend/src/services/ai/prompt-modules/role-boundary.module.js +172 -0
  91. package/dist/backend/backend/src/services/ai/prompt-modules/role-boundary.module.js.map +1 -0
  92. package/dist/backend/backend/src/services/ai/prompt-modules/user-profile-reference.module.d.ts +5 -1
  93. package/dist/backend/backend/src/services/ai/prompt-modules/user-profile-reference.module.d.ts.map +1 -1
  94. package/dist/backend/backend/src/services/ai/prompt-modules/user-profile-reference.module.js +18 -2
  95. package/dist/backend/backend/src/services/ai/prompt-modules/user-profile-reference.module.js.map +1 -1
  96. package/dist/backend/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -1
  97. package/dist/backend/backend/src/services/cloud/cloud-client.service.js +9 -2
  98. package/dist/backend/backend/src/services/cloud/cloud-client.service.js.map +1 -1
  99. package/dist/backend/backend/src/services/cloud/relay-client.service.d.ts +11 -0
  100. package/dist/backend/backend/src/services/cloud/relay-client.service.d.ts.map +1 -1
  101. package/dist/backend/backend/src/services/cloud/relay-client.service.js +41 -5
  102. package/dist/backend/backend/src/services/cloud/relay-client.service.js.map +1 -1
  103. package/dist/backend/backend/src/services/memory/agent-memory.service.d.ts +22 -0
  104. package/dist/backend/backend/src/services/memory/agent-memory.service.d.ts.map +1 -1
  105. package/dist/backend/backend/src/services/memory/agent-memory.service.js +103 -6
  106. package/dist/backend/backend/src/services/memory/agent-memory.service.js.map +1 -1
  107. package/dist/backend/backend/src/services/memory/index.d.ts +2 -1
  108. package/dist/backend/backend/src/services/memory/index.d.ts.map +1 -1
  109. package/dist/backend/backend/src/services/memory/index.js +1 -0
  110. package/dist/backend/backend/src/services/memory/index.js.map +1 -1
  111. package/dist/backend/backend/src/services/memory/memory.service.d.ts +43 -0
  112. package/dist/backend/backend/src/services/memory/memory.service.d.ts.map +1 -1
  113. package/dist/backend/backend/src/services/memory/memory.service.js +96 -0
  114. package/dist/backend/backend/src/services/memory/memory.service.js.map +1 -1
  115. package/dist/backend/backend/src/services/memory/user-profile.service.d.ts +77 -0
  116. package/dist/backend/backend/src/services/memory/user-profile.service.d.ts.map +1 -0
  117. package/dist/backend/backend/src/services/memory/user-profile.service.js +171 -0
  118. package/dist/backend/backend/src/services/memory/user-profile.service.js.map +1 -0
  119. package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts +8 -3
  120. package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
  121. package/dist/backend/backend/src/services/messaging/queue-processor.service.js +41 -16
  122. package/dist/backend/backend/src/services/messaging/queue-processor.service.js.map +1 -1
  123. package/dist/backend/backend/src/services/messaging/thread-status-queue.service.d.ts +144 -1
  124. package/dist/backend/backend/src/services/messaging/thread-status-queue.service.d.ts.map +1 -1
  125. package/dist/backend/backend/src/services/messaging/thread-status-queue.service.js +259 -2
  126. package/dist/backend/backend/src/services/messaging/thread-status-queue.service.js.map +1 -1
  127. package/dist/backend/backend/src/services/project/task-tracking.service.d.ts +58 -2
  128. package/dist/backend/backend/src/services/project/task-tracking.service.d.ts.map +1 -1
  129. package/dist/backend/backend/src/services/project/task-tracking.service.js +189 -1
  130. package/dist/backend/backend/src/services/project/task-tracking.service.js.map +1 -1
  131. package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts.map +1 -1
  132. package/dist/backend/backend/src/services/workflow/cron-task.service.js +12 -0
  133. package/dist/backend/backend/src/services/workflow/cron-task.service.js.map +1 -1
  134. package/dist/backend/backend/src/types/event-bus.types.d.ts +1 -1
  135. package/dist/backend/backend/src/types/event-bus.types.d.ts.map +1 -1
  136. package/dist/backend/backend/src/types/event-bus.types.js +14 -0
  137. package/dist/backend/backend/src/types/event-bus.types.js.map +1 -1
  138. package/dist/backend/backend/src/types/index.d.ts +12 -0
  139. package/dist/backend/backend/src/types/index.d.ts.map +1 -1
  140. package/dist/backend/backend/src/types/index.js.map +1 -1
  141. package/dist/backend/backend/src/types/memory.types.d.ts +53 -0
  142. package/dist/backend/backend/src/types/memory.types.d.ts.map +1 -1
  143. package/dist/backend/backend/src/types/memory.types.js.map +1 -1
  144. package/dist/backend/backend/src/types/task-tracking.types.d.ts +28 -1
  145. package/dist/backend/backend/src/types/task-tracking.types.d.ts.map +1 -1
  146. package/dist/backend/backend/src/types/task-tracking.types.js.map +1 -1
  147. package/dist/backend/backend/src/types/thread-status.types.d.ts +4 -0
  148. package/dist/backend/backend/src/types/thread-status.types.d.ts.map +1 -1
  149. package/dist/backend/backend/src/types/thread-status.types.js +4 -1
  150. package/dist/backend/backend/src/types/thread-status.types.js.map +1 -1
  151. package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts +157 -0
  152. package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.d.ts.map +1 -0
  153. package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.js +37 -0
  154. package/dist/cli/backend/src/services/ai/prompt-modules/prompt-module.interface.js.map +1 -0
  155. package/dist/cli/backend/src/services/memory/agent-memory.service.d.ts +22 -0
  156. package/dist/cli/backend/src/services/memory/agent-memory.service.d.ts.map +1 -1
  157. package/dist/cli/backend/src/services/memory/agent-memory.service.js +103 -6
  158. package/dist/cli/backend/src/services/memory/agent-memory.service.js.map +1 -1
  159. package/dist/cli/backend/src/services/memory/goal-tracking.service.d.ts +239 -0
  160. package/dist/cli/backend/src/services/memory/goal-tracking.service.d.ts.map +1 -0
  161. package/dist/cli/backend/src/services/memory/goal-tracking.service.js +353 -0
  162. package/dist/cli/backend/src/services/memory/goal-tracking.service.js.map +1 -0
  163. package/dist/cli/backend/src/services/memory/memory.service.d.ts +43 -0
  164. package/dist/cli/backend/src/services/memory/memory.service.d.ts.map +1 -1
  165. package/dist/cli/backend/src/services/memory/memory.service.js +96 -0
  166. package/dist/cli/backend/src/services/memory/memory.service.js.map +1 -1
  167. package/dist/cli/backend/src/services/project/task-tracking.service.d.ts +171 -0
  168. package/dist/cli/backend/src/services/project/task-tracking.service.d.ts.map +1 -0
  169. package/dist/cli/backend/src/services/project/task-tracking.service.js +725 -0
  170. package/dist/cli/backend/src/services/project/task-tracking.service.js.map +1 -0
  171. package/dist/cli/backend/src/types/index.d.ts +12 -0
  172. package/dist/cli/backend/src/types/index.d.ts.map +1 -1
  173. package/dist/cli/backend/src/types/index.js.map +1 -1
  174. package/dist/cli/backend/src/types/memory.types.d.ts +53 -0
  175. package/dist/cli/backend/src/types/memory.types.d.ts.map +1 -1
  176. package/dist/cli/backend/src/types/memory.types.js.map +1 -1
  177. package/dist/cli/backend/src/types/task-tracking.types.d.ts +206 -0
  178. package/dist/cli/backend/src/types/task-tracking.types.d.ts.map +1 -0
  179. package/dist/cli/backend/src/types/task-tracking.types.js +5 -0
  180. package/dist/cli/backend/src/types/task-tracking.types.js.map +1 -0
  181. package/dist/cli/backend/src/types/thread-status.types.d.ts +4 -0
  182. package/dist/cli/backend/src/types/thread-status.types.d.ts.map +1 -1
  183. package/dist/cli/backend/src/types/thread-status.types.js +4 -1
  184. package/dist/cli/backend/src/types/thread-status.types.js.map +1 -1
  185. package/frontend/dist/assets/index-512efc8e.js +4921 -0
  186. package/frontend/dist/assets/{index-975ccc95.css → index-dc6ac165.css} +1 -1
  187. package/frontend/dist/index.html +2 -2
  188. package/package.json +1 -1
  189. 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