crewly 1.5.11 → 1.5.13

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 (176) hide show
  1. package/config/constants.ts +3 -3
  2. package/config/orchestrator_tasks/prompts/orchestrator-prompt.md +73 -0
  3. package/config/roles/architect/prompt.md +9 -0
  4. package/config/roles/backend-developer/prompt.md +9 -0
  5. package/config/roles/content-strategist/prompt.md +10 -0
  6. package/config/roles/designer/prompt.md +9 -0
  7. package/config/roles/developer/prompt.md +9 -0
  8. package/config/roles/frontend-developer/prompt.md +9 -0
  9. package/config/roles/fullstack-dev/prompt.md +9 -0
  10. package/config/roles/generalist/prompt.md +9 -0
  11. package/config/roles/ops/prompt.md +9 -0
  12. package/config/roles/product-manager/prompt.md +9 -0
  13. package/config/roles/qa/prompt.md +9 -0
  14. package/config/roles/qa-engineer/prompt.md +9 -0
  15. package/config/roles/researcher/prompt.md +9 -0
  16. package/config/roles/sales/prompt.md +9 -0
  17. package/config/roles/support/prompt.md +9 -0
  18. package/config/roles/team-leader/prompt.md +11 -0
  19. package/config/roles/tpm/prompt.md +9 -0
  20. package/config/roles/ux-designer/prompt.md +9 -0
  21. package/config/skills/_common/lib.sh +31 -0
  22. package/config/skills/_common/lib.test.sh +164 -0
  23. package/config/skills/agent/core/block-task/execute.sh +3 -1
  24. package/config/skills/agent/core/pipe-to-sink/execute.sh +41 -0
  25. package/config/skills/agent/core/read-task/execute.sh +3 -1
  26. package/config/skills/agent/core/report-progress/execute.sh +3 -1
  27. package/config/skills/agent/screenshot-compare/SKILL.md +75 -0
  28. package/config/skills/agent/screenshot-compare/execute.sh +182 -0
  29. package/config/skills/agent/screenshot-compare/skill.json +10 -0
  30. package/config/skills/agent/xiaoyuzhoufm-transcript/SKILL.md +85 -0
  31. package/config/skills/agent/xiaoyuzhoufm-transcript/execute.sh +306 -0
  32. package/config/skills/agent/xiaoyuzhoufm-transcript/skill.json +10 -0
  33. package/config/skills/orchestrator/cancel-cron/SKILL.md +44 -0
  34. package/config/skills/orchestrator/create-cron/SKILL.md +58 -0
  35. package/config/skills/orchestrator/list-cron/SKILL.md +51 -0
  36. package/config/skills/orchestrator/update-cron/SKILL.md +52 -0
  37. package/dist/backend/backend/src/constants.d.ts +7 -4
  38. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  39. package/dist/backend/backend/src/constants.js +6 -3
  40. package/dist/backend/backend/src/constants.js.map +1 -1
  41. package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts +21 -2
  42. package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
  43. package/dist/backend/backend/src/controllers/browser/browser.controller.js +167 -29
  44. package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
  45. package/dist/backend/backend/src/controllers/browser/browser.routes.d.ts +1 -1
  46. package/dist/backend/backend/src/controllers/browser/browser.routes.d.ts.map +1 -1
  47. package/dist/backend/backend/src/controllers/browser/browser.routes.js +7 -3
  48. package/dist/backend/backend/src/controllers/browser/browser.routes.js.map +1 -1
  49. package/dist/backend/backend/src/controllers/data/data.controller.d.ts +47 -0
  50. package/dist/backend/backend/src/controllers/data/data.controller.d.ts.map +1 -0
  51. package/dist/backend/backend/src/controllers/data/data.controller.js +201 -0
  52. package/dist/backend/backend/src/controllers/data/data.controller.js.map +1 -0
  53. package/dist/backend/backend/src/controllers/data/data.routes.d.ts +18 -0
  54. package/dist/backend/backend/src/controllers/data/data.routes.d.ts.map +1 -0
  55. package/dist/backend/backend/src/controllers/data/data.routes.js +44 -0
  56. package/dist/backend/backend/src/controllers/data/data.routes.js.map +1 -0
  57. package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.d.ts +3 -2
  58. package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.d.ts.map +1 -1
  59. package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.js +5 -3
  60. package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.js.map +1 -1
  61. package/dist/backend/backend/src/controllers/system/cron-task.controller.d.ts +4 -0
  62. package/dist/backend/backend/src/controllers/system/cron-task.controller.d.ts.map +1 -1
  63. package/dist/backend/backend/src/controllers/system/cron-task.controller.js +20 -0
  64. package/dist/backend/backend/src/controllers/system/cron-task.controller.js.map +1 -1
  65. package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +1 -1
  66. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +18 -0
  67. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +1 -1
  68. package/dist/backend/backend/src/controllers/team/team-export.controller.d.ts +32 -0
  69. package/dist/backend/backend/src/controllers/team/team-export.controller.d.ts.map +1 -0
  70. package/dist/backend/backend/src/controllers/team/team-export.controller.js +61 -0
  71. package/dist/backend/backend/src/controllers/team/team-export.controller.js.map +1 -0
  72. package/dist/backend/backend/src/controllers/team/team.routes.d.ts.map +1 -1
  73. package/dist/backend/backend/src/controllers/team/team.routes.js +7 -0
  74. package/dist/backend/backend/src/controllers/team/team.routes.js.map +1 -1
  75. package/dist/backend/backend/src/index.d.ts.map +1 -1
  76. package/dist/backend/backend/src/index.js +37 -7
  77. package/dist/backend/backend/src/index.js.map +1 -1
  78. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  79. package/dist/backend/backend/src/routes/api.routes.js +4 -1
  80. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  81. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  82. package/dist/backend/backend/src/services/agent/agent-registration.service.js +6 -2
  83. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  84. package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts.map +1 -1
  85. package/dist/backend/backend/src/services/agent/idle-detection.service.js +17 -2
  86. package/dist/backend/backend/src/services/agent/idle-detection.service.js.map +1 -1
  87. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts +1 -1
  88. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +2 -2
  89. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
  90. package/dist/backend/backend/src/services/agent/task-planning.service.d.ts +134 -0
  91. package/dist/backend/backend/src/services/agent/task-planning.service.d.ts.map +1 -0
  92. package/dist/backend/backend/src/services/agent/task-planning.service.js +291 -0
  93. package/dist/backend/backend/src/services/agent/task-planning.service.js.map +1 -0
  94. package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.d.ts +11 -0
  95. package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.d.ts.map +1 -1
  96. package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js +47 -18
  97. package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js.map +1 -1
  98. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
  99. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  100. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +47 -4
  101. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  102. package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts +13 -9
  103. package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts.map +1 -1
  104. package/dist/backend/backend/src/services/browser/browser-bridge.service.js +44 -12
  105. package/dist/backend/backend/src/services/browser/browser-bridge.service.js.map +1 -1
  106. package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts +176 -0
  107. package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -0
  108. package/dist/backend/backend/src/services/browser/browser-proxy.service.js +441 -0
  109. package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -0
  110. package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts +162 -0
  111. package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts.map +1 -0
  112. package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js +350 -0
  113. package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js.map +1 -0
  114. package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts +8 -0
  115. package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts.map +1 -1
  116. package/dist/backend/backend/src/services/cloud/cloud-initializer.js +27 -0
  117. package/dist/backend/backend/src/services/cloud/cloud-initializer.js.map +1 -1
  118. package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts +1 -1
  119. package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
  120. package/dist/backend/backend/src/services/cloud/cloud-sync.types.js +2 -0
  121. package/dist/backend/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
  122. package/dist/backend/backend/src/services/core/team-export.service.d.ts +103 -0
  123. package/dist/backend/backend/src/services/core/team-export.service.d.ts.map +1 -0
  124. package/dist/backend/backend/src/services/core/team-export.service.js +182 -0
  125. package/dist/backend/backend/src/services/core/team-export.service.js.map +1 -0
  126. package/dist/backend/backend/src/services/data/data-object-store.service.d.ts +160 -0
  127. package/dist/backend/backend/src/services/data/data-object-store.service.d.ts.map +1 -0
  128. package/dist/backend/backend/src/services/data/data-object-store.service.js +434 -0
  129. package/dist/backend/backend/src/services/data/data-object-store.service.js.map +1 -0
  130. package/dist/backend/backend/src/services/data/data-object.types.d.ts +190 -0
  131. package/dist/backend/backend/src/services/data/data-object.types.d.ts.map +1 -0
  132. package/dist/backend/backend/src/services/data/data-object.types.js +143 -0
  133. package/dist/backend/backend/src/services/data/data-object.types.js.map +1 -0
  134. package/dist/backend/backend/src/services/data/schema-registry.service.d.ts +108 -0
  135. package/dist/backend/backend/src/services/data/schema-registry.service.d.ts.map +1 -0
  136. package/dist/backend/backend/src/services/data/schema-registry.service.js +290 -0
  137. package/dist/backend/backend/src/services/data/schema-registry.service.js.map +1 -0
  138. package/dist/backend/backend/src/services/data/sink-registry.service.d.ts +87 -0
  139. package/dist/backend/backend/src/services/data/sink-registry.service.d.ts.map +1 -0
  140. package/dist/backend/backend/src/services/data/sink-registry.service.js +188 -0
  141. package/dist/backend/backend/src/services/data/sink-registry.service.js.map +1 -0
  142. package/dist/backend/backend/src/services/messaging/message-router.service.d.ts.map +1 -1
  143. package/dist/backend/backend/src/services/messaging/message-router.service.js +7 -0
  144. package/dist/backend/backend/src/services/messaging/message-router.service.js.map +1 -1
  145. package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts +55 -2
  146. package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
  147. package/dist/backend/backend/src/services/monitoring/token-usage.service.js +89 -5
  148. package/dist/backend/backend/src/services/monitoring/token-usage.service.js.map +1 -1
  149. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +1 -1
  150. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  151. package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts +105 -14
  152. package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts.map +1 -1
  153. package/dist/backend/backend/src/services/workflow/cron-task.service.js +400 -123
  154. package/dist/backend/backend/src/services/workflow/cron-task.service.js.map +1 -1
  155. package/dist/backend/backend/src/types/cron-task.types.d.ts +1 -1
  156. package/dist/backend/backend/src/types/data-object.types.d.ts +117 -0
  157. package/dist/backend/backend/src/types/data-object.types.d.ts.map +1 -0
  158. package/dist/backend/backend/src/types/data-object.types.js +23 -0
  159. package/dist/backend/backend/src/types/data-object.types.js.map +1 -0
  160. package/dist/backend/backend/src/types/settings.types.js +1 -1
  161. package/dist/backend/config/constants.d.ts +3 -3
  162. package/dist/backend/config/constants.js +3 -3
  163. package/dist/backend/config/constants.js.map +1 -1
  164. package/dist/cli/backend/src/constants.d.ts +7 -4
  165. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  166. package/dist/cli/backend/src/constants.js +6 -3
  167. package/dist/cli/backend/src/constants.js.map +1 -1
  168. package/dist/cli/backend/src/types/settings.types.js +1 -1
  169. package/dist/cli/config/constants.d.ts +3 -3
  170. package/dist/cli/config/constants.js +3 -3
  171. package/dist/cli/config/constants.js.map +1 -1
  172. package/frontend/dist/assets/index-371b68d4.css +33 -0
  173. package/frontend/dist/assets/{index-9af2ea40.js → index-506f70da.js} +321 -321
  174. package/frontend/dist/index.html +2 -2
  175. package/package.json +1 -1
  176. package/frontend/dist/assets/index-b19b2478.css +0 -33
@@ -5,19 +5,27 @@
5
5
  * Unlike scheduled messages (agent-created/cancellable), cron tasks
6
6
  * can only be cancelled by user or orchestrator.
7
7
  *
8
- * Storage: ~/.crewly/cron-tasks.json
8
+ * Storage: Per-team at ~/.crewly/teams/{teamId}/cron-tasks.json
9
+ * Migration: Global ~/.crewly/cron-tasks.json is auto-migrated on first startup.
9
10
  *
10
11
  * @module services/workflow/cron-task.service
11
12
  */
12
13
  import * as os from 'os';
13
14
  import * as path from 'path';
14
- import { readFile, writeFile, mkdir } from 'fs/promises';
15
+ import { existsSync } from 'fs';
16
+ import { readFile, writeFile, mkdir, readdir } from 'fs/promises';
15
17
  import { v4 as uuidv4 } from 'uuid';
16
18
  import { LoggerService } from '../core/logger.service.js';
17
19
  /**
18
20
  * Check interval for cron task evaluation (60 seconds).
19
21
  */
20
22
  const CRON_CHECK_INTERVAL_MS = 60_000;
23
+ /** Name of the per-team cron task file */
24
+ const CRON_TASKS_FILENAME = 'cron-tasks.json';
25
+ /** Suffix appended to the global file after successful migration */
26
+ const MIGRATED_SUFFIX = '.migrated';
27
+ /** Team ID value used for orchestrator-level cron tasks (not team-scoped) */
28
+ const ORCHESTRATOR_TEAM_ID = 'orchestrator';
21
29
  /**
22
30
  * Extract date/time parts from a Date in a specific IANA timezone.
23
31
  *
@@ -153,26 +161,35 @@ export function getNextRunTime(cronExpression, timezone, after) {
153
161
  return new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString();
154
162
  }
155
163
  /**
156
- * Service for managing cron tasks.
164
+ * Service for managing cron tasks with per-team scoped storage.
165
+ *
166
+ * Storage layout:
167
+ * ~/.crewly/teams/{teamId}/cron-tasks.json
168
+ *
169
+ * On first startup, migrates the legacy global file
170
+ * (~/.crewly/cron-tasks.json) by grouping tasks by targetTeamId
171
+ * and writing per-team files. The global file is renamed to
172
+ * cron-tasks.json.migrated to prevent re-migration.
157
173
  *
158
174
  * Lifecycle:
159
175
  * 1. Get singleton via `getInstance()`
160
176
  * 2. Wire callbacks: `setExecutionCallback()`, `setAgentStatusCallback()`, `setAgentStartCallback()`
161
- * 3. Call `start()` to begin the evaluation loop
162
- * 4. On startup, call `recalculateAllNextRunTimes()` to self-heal stale values
177
+ * 3. Call `migrateGlobalToTeamScoped()` to auto-migrate legacy storage
178
+ * 4. Call `start()` to begin the evaluation loop
179
+ * 5. On startup, call `recalculateAllNextRunTimes()` to self-heal stale values
163
180
  */
164
181
  export class CronTaskService {
165
182
  static instance = null;
166
183
  logger;
167
- storeFile;
184
+ /** Root crewly home directory (e.g. ~/.crewly) */
185
+ crewlyHome;
168
186
  timer = null;
169
187
  executionCallback = null;
170
188
  agentStatusCallback = null;
171
189
  agentStartCallback = null;
172
190
  constructor(crewlyHome) {
173
191
  this.logger = LoggerService.getInstance().createComponentLogger('CronTaskService');
174
- const home = crewlyHome || path.join(os.homedir(), '.crewly');
175
- this.storeFile = path.join(home, 'cron-tasks.json');
192
+ this.crewlyHome = crewlyHome || path.join(os.homedir(), '.crewly');
176
193
  }
177
194
  /**
178
195
  * Get singleton instance.
@@ -192,6 +209,98 @@ export class CronTaskService {
192
209
  }
193
210
  CronTaskService.instance = null;
194
211
  }
212
+ /**
213
+ * Get the store file path for a specific team.
214
+ *
215
+ * @param teamId - Team ID
216
+ * @returns Absolute path to the team's cron-tasks.json
217
+ */
218
+ getStoreFile(teamId) {
219
+ return path.join(this.crewlyHome, 'teams', teamId, CRON_TASKS_FILENAME);
220
+ }
221
+ /**
222
+ * Get the legacy global store file path.
223
+ *
224
+ * @returns Absolute path to the global cron-tasks.json
225
+ */
226
+ getGlobalStoreFile() {
227
+ return path.join(this.crewlyHome, CRON_TASKS_FILENAME);
228
+ }
229
+ /**
230
+ * Migrate legacy global cron-tasks.json to per-team files.
231
+ *
232
+ * Orchestrator-targeted tasks (targetTeamId === 'orchestrator') are kept
233
+ * in the global file since they don't belong to any specific team.
234
+ * All other tasks are grouped by targetTeamId and written to per-team files.
235
+ *
236
+ * After migration, the global file is rewritten to contain only orchestrator
237
+ * tasks, and a .migrated marker is created to prevent re-running.
238
+ *
239
+ * Idempotent: skips if the .migrated marker already exists or no global file exists.
240
+ *
241
+ * @returns Number of tasks migrated to per-team files (excludes orchestrator tasks kept in global)
242
+ */
243
+ async migrateGlobalToTeamScoped() {
244
+ const globalFile = this.getGlobalStoreFile();
245
+ const migratedFile = globalFile + MIGRATED_SUFFIX;
246
+ // Skip if already migrated or no global file exists
247
+ if (existsSync(migratedFile) || !existsSync(globalFile)) {
248
+ return 0;
249
+ }
250
+ let globalStore;
251
+ try {
252
+ const data = await readFile(globalFile, 'utf-8');
253
+ globalStore = JSON.parse(data);
254
+ }
255
+ catch {
256
+ // Cannot read/parse global file — nothing to migrate
257
+ return 0;
258
+ }
259
+ if (globalStore.tasks.length === 0) {
260
+ // Empty store — write marker and skip
261
+ await writeFile(migratedFile, '', 'utf-8');
262
+ return 0;
263
+ }
264
+ // Partition: orchestrator tasks stay global, everything else goes per-team
265
+ const orchestratorTasks = [];
266
+ const teamGroups = new Map();
267
+ for (const task of globalStore.tasks) {
268
+ if (task.targetTeamId === ORCHESTRATOR_TEAM_ID) {
269
+ orchestratorTasks.push(task);
270
+ }
271
+ else {
272
+ const teamId = task.targetTeamId;
273
+ if (!teamGroups.has(teamId)) {
274
+ teamGroups.set(teamId, []);
275
+ }
276
+ teamGroups.get(teamId).push(task);
277
+ }
278
+ }
279
+ // Write team-scoped tasks to per-team files
280
+ let migratedCount = 0;
281
+ for (const [teamId, tasks] of teamGroups) {
282
+ const teamStore = await this.loadTeamStore(teamId);
283
+ // Merge: add migrated tasks that don't already exist
284
+ const existingIds = new Set(teamStore.tasks.map(t => t.id));
285
+ for (const task of tasks) {
286
+ if (!existingIds.has(task.id)) {
287
+ teamStore.tasks.push(task);
288
+ migratedCount++;
289
+ }
290
+ }
291
+ await this.saveTeamStore(teamId, teamStore);
292
+ }
293
+ // Rewrite global file with only orchestrator tasks
294
+ await this.saveGlobalStore({ tasks: orchestratorTasks });
295
+ // Write migration marker to prevent re-running
296
+ await writeFile(migratedFile, new Date().toISOString(), 'utf-8');
297
+ this.logger.info('Migrated global cron-tasks.json to per-team storage', {
298
+ migratedCount,
299
+ orchestratorKept: orchestratorTasks.length,
300
+ teamCount: teamGroups.size,
301
+ });
302
+ return migratedCount;
303
+ }
195
304
  /**
196
305
  * Set the callback invoked when a cron task is due for execution.
197
306
  * The callback receives the task and should delegate it to the target agent.
@@ -265,15 +374,15 @@ export class CronTaskService {
265
374
  * @returns Number of tasks whose nextRunAt was updated
266
375
  */
267
376
  async recalculateAllNextRunTimes() {
268
- const store = await this.loadStore();
377
+ const allTasks = await this.loadAllTasks();
269
378
  const now = new Date();
270
379
  let updated = 0;
271
- for (const task of store.tasks) {
380
+ // Group tasks back by team for saving
381
+ const dirtyTeams = new Set();
382
+ for (const task of allTasks) {
272
383
  if (!task.enabled)
273
384
  continue;
274
385
  // If task has never run AND its nextRunAt is in the past, this is a missed first run.
275
- // Keep the stale nextRunAt so evaluateTasks() picks it up and fires immediately,
276
- // rather than silently advancing to the next occurrence.
277
386
  if (!task.lastRunAt && task.nextRunAt && new Date(task.nextRunAt) <= now) {
278
387
  this.logger.info('Missed first run detected — keeping stale nextRunAt for immediate execution', {
279
388
  id: task.id,
@@ -293,10 +402,15 @@ export class CronTaskService {
293
402
  });
294
403
  task.nextRunAt = recalculated;
295
404
  updated++;
405
+ dirtyTeams.add(task.targetTeamId);
296
406
  }
297
407
  }
298
- if (updated > 0) {
299
- await this.saveStore(store);
408
+ // Save only modified team stores
409
+ if (dirtyTeams.size > 0) {
410
+ for (const teamId of dirtyTeams) {
411
+ const teamTasks = allTasks.filter(t => t.targetTeamId === teamId);
412
+ await this.saveTeamStore(teamId, { tasks: teamTasks });
413
+ }
300
414
  this.logger.info('Recalculated nextRunAt for stale tasks', { count: updated });
301
415
  }
302
416
  return updated;
@@ -323,20 +437,45 @@ export class CronTaskService {
323
437
  lastRunAt: null,
324
438
  nextRunAt,
325
439
  };
326
- const store = await this.loadStore();
327
- store.tasks.push(task);
328
- await this.saveStore(store);
329
- this.logger.info('Cron task created', { id: task.id, cron: task.cronExpression, target: task.targetAgent });
440
+ // Orchestrator tasks go to global store; team tasks go to per-team store
441
+ if (task.targetTeamId === ORCHESTRATOR_TEAM_ID) {
442
+ const store = await this.loadGlobalStore();
443
+ store.tasks.push(task);
444
+ await this.saveGlobalStore(store);
445
+ }
446
+ else {
447
+ const store = await this.loadTeamStore(task.targetTeamId);
448
+ store.tasks.push(task);
449
+ await this.saveTeamStore(task.targetTeamId, store);
450
+ }
451
+ this.logger.info('Cron task created', { id: task.id, cron: task.cronExpression, target: task.targetAgent, teamId: task.targetTeamId });
330
452
  return task;
331
453
  }
332
454
  /**
333
- * List all cron tasks, optionally filtered.
455
+ * List all cron tasks across all teams, optionally filtered.
334
456
  *
335
457
  * @param filter - Optional filter criteria
336
458
  * @returns Array of matching cron tasks
337
459
  */
338
460
  async list(filter) {
339
- const store = await this.loadStore();
461
+ let tasks = await this.loadAllTasks();
462
+ if (filter?.targetAgent) {
463
+ tasks = tasks.filter(t => t.targetAgent === filter.targetAgent);
464
+ }
465
+ if (filter?.enabled !== undefined) {
466
+ tasks = tasks.filter(t => t.enabled === filter.enabled);
467
+ }
468
+ return tasks;
469
+ }
470
+ /**
471
+ * List cron tasks for a specific team.
472
+ *
473
+ * @param teamId - Team ID to filter by
474
+ * @param filter - Optional additional filter criteria
475
+ * @returns Array of matching cron tasks for the team
476
+ */
477
+ async getByTeam(teamId, filter) {
478
+ const store = await this.loadTeamStore(teamId);
340
479
  let tasks = store.tasks;
341
480
  if (filter?.targetAgent) {
342
481
  tasks = tasks.filter(t => t.targetAgent === filter.targetAgent);
@@ -347,61 +486,92 @@ export class CronTaskService {
347
486
  return tasks;
348
487
  }
349
488
  /**
350
- * Get a single cron task by ID.
489
+ * Get a single cron task by ID. Searches across all teams.
351
490
  *
352
491
  * @param id - Cron task ID
353
492
  * @returns The cron task or null
354
493
  */
355
494
  async get(id) {
356
- const store = await this.loadStore();
357
- return store.tasks.find(t => t.id === id) || null;
495
+ const allTasks = await this.loadAllTasks();
496
+ return allTasks.find(t => t.id === id) || null;
358
497
  }
359
498
  /**
360
- * Update a cron task. Agents can update description/expression/enabled
361
- * but cannot delete.
499
+ * Update a cron task. Searches across all teams to find the task.
362
500
  *
363
501
  * @param id - Cron task ID
364
502
  * @param updates - Fields to update
365
503
  * @returns Updated task or null if not found
366
504
  */
367
505
  async update(id, updates) {
368
- const store = await this.loadStore();
369
- const task = store.tasks.find(t => t.id === id);
370
- if (!task)
371
- return null;
372
- if (updates.cronExpression !== undefined) {
373
- task.cronExpression = updates.cronExpression;
374
- task.nextRunAt = getNextRunTime(updates.cronExpression, task.timezone);
375
- }
376
- if (updates.timezone !== undefined) {
377
- task.timezone = updates.timezone;
378
- task.nextRunAt = getNextRunTime(task.cronExpression, task.timezone);
379
- }
380
- if (updates.taskDescription !== undefined) {
381
- task.taskDescription = updates.taskDescription;
382
- }
383
- if (updates.enabled !== undefined) {
384
- task.enabled = updates.enabled;
385
- }
386
- await this.saveStore(store);
387
- this.logger.info('Cron task updated', { id, updates: Object.keys(updates) });
388
- return task;
506
+ // Helper to apply updates to a task
507
+ const applyUpdates = (task) => {
508
+ if (updates.cronExpression !== undefined) {
509
+ task.cronExpression = updates.cronExpression;
510
+ task.nextRunAt = getNextRunTime(updates.cronExpression, task.timezone);
511
+ }
512
+ if (updates.timezone !== undefined) {
513
+ task.timezone = updates.timezone;
514
+ task.nextRunAt = getNextRunTime(task.cronExpression, task.timezone);
515
+ }
516
+ if (updates.taskDescription !== undefined) {
517
+ task.taskDescription = updates.taskDescription;
518
+ }
519
+ if (updates.enabled !== undefined) {
520
+ task.enabled = updates.enabled;
521
+ }
522
+ };
523
+ // Search global store first (orchestrator tasks)
524
+ const globalStore = await this.loadGlobalStore();
525
+ const globalTask = globalStore.tasks.find(t => t.id === id);
526
+ if (globalTask) {
527
+ applyUpdates(globalTask);
528
+ await this.saveGlobalStore(globalStore);
529
+ this.logger.info('Cron task updated', { id, location: 'global', updates: Object.keys(updates) });
530
+ return globalTask;
531
+ }
532
+ // Search per-team stores
533
+ const teamIds = await this.getTeamIds();
534
+ for (const teamId of teamIds) {
535
+ const store = await this.loadTeamStore(teamId);
536
+ const task = store.tasks.find(t => t.id === id);
537
+ if (!task)
538
+ continue;
539
+ applyUpdates(task);
540
+ await this.saveTeamStore(teamId, store);
541
+ this.logger.info('Cron task updated', { id, teamId, updates: Object.keys(updates) });
542
+ return task;
543
+ }
544
+ return null;
389
545
  }
390
546
  /**
391
- * Delete a cron task. Only user/orchestrator should call this.
547
+ * Delete a cron task. Searches across all teams to find and remove it.
392
548
  *
393
549
  * @param id - Cron task ID
394
550
  * @returns true if deleted, false if not found
395
551
  */
396
552
  async delete(id) {
397
- const store = await this.loadStore();
398
- const index = store.tasks.findIndex(t => t.id === id);
399
- if (index === -1)
400
- return false;
401
- store.tasks.splice(index, 1);
402
- await this.saveStore(store);
403
- this.logger.info('Cron task deleted', { id });
404
- return true;
553
+ // Search global store first (orchestrator tasks)
554
+ const globalStore = await this.loadGlobalStore();
555
+ const globalIndex = globalStore.tasks.findIndex(t => t.id === id);
556
+ if (globalIndex !== -1) {
557
+ globalStore.tasks.splice(globalIndex, 1);
558
+ await this.saveGlobalStore(globalStore);
559
+ this.logger.info('Cron task deleted', { id, location: 'global' });
560
+ return true;
561
+ }
562
+ // Search per-team stores
563
+ const teamIds = await this.getTeamIds();
564
+ for (const teamId of teamIds) {
565
+ const store = await this.loadTeamStore(teamId);
566
+ const index = store.tasks.findIndex(t => t.id === id);
567
+ if (index === -1)
568
+ continue;
569
+ store.tasks.splice(index, 1);
570
+ await this.saveTeamStore(teamId, store);
571
+ this.logger.info('Cron task deleted', { id, teamId });
572
+ return true;
573
+ }
574
+ return false;
405
575
  }
406
576
  /**
407
577
  * Evaluate all enabled tasks and execute those whose nextRunAt has passed.
@@ -410,95 +580,153 @@ export class CronTaskService {
410
580
  * attempts to auto-start the agent before executing.
411
581
  */
412
582
  async evaluateTasks() {
413
- const store = await this.loadStore();
414
583
  const now = new Date();
415
- let updated = false;
416
- for (const task of store.tasks) {
417
- if (!task.enabled || !task.nextRunAt)
418
- continue;
419
- const nextRun = new Date(task.nextRunAt);
420
- if (nextRun > now)
421
- continue;
422
- // Task is due — check agent status before executing
423
- let agentOnline = true;
424
- if (this.agentStatusCallback) {
425
- try {
426
- agentOnline = await this.agentStatusCallback(task.targetAgent, task.targetTeamId);
427
- }
428
- catch (error) {
429
- this.logger.warn('Agent status check failed, assuming offline', {
430
- id: task.id,
431
- target: task.targetAgent,
432
- error: error instanceof Error ? error.message : String(error),
433
- });
434
- agentOnline = false;
435
- }
584
+ // Evaluate global orchestrator tasks
585
+ const globalStore = await this.loadGlobalStore();
586
+ let globalUpdated = false;
587
+ for (const task of globalStore.tasks) {
588
+ const result = await this.evaluateSingleTask(task, now);
589
+ if (result)
590
+ globalUpdated = true;
591
+ }
592
+ if (globalUpdated) {
593
+ await this.saveGlobalStore(globalStore);
594
+ }
595
+ // Evaluate per-team tasks
596
+ const teamIds = await this.getTeamIds();
597
+ for (const teamId of teamIds) {
598
+ const store = await this.loadTeamStore(teamId);
599
+ let updated = false;
600
+ for (const task of store.tasks) {
601
+ const result = await this.evaluateSingleTask(task, now);
602
+ if (result)
603
+ updated = true;
436
604
  }
437
- // Auto-start offline agent if callback is available
438
- if (!agentOnline && this.agentStartCallback) {
439
- this.logger.info('Agent offline for cron task, attempting auto-start', {
440
- id: task.id,
441
- target: task.targetAgent,
442
- });
443
- try {
444
- const started = await this.agentStartCallback(task.targetAgent, task.targetTeamId);
445
- if (started) {
446
- agentOnline = true;
447
- this.logger.info('Agent auto-started successfully', {
448
- id: task.id,
449
- target: task.targetAgent,
450
- });
451
- }
452
- }
453
- catch (error) {
454
- this.logger.error('Agent auto-start failed', {
455
- id: task.id,
456
- target: task.targetAgent,
457
- error: error instanceof Error ? error.message : String(error),
458
- });
459
- }
605
+ if (updated) {
606
+ await this.saveTeamStore(teamId, store);
607
+ }
608
+ }
609
+ }
610
+ /**
611
+ * Evaluate a single cron task: check if due, verify agent status, execute.
612
+ * Mutates the task in-place (lastRunAt, nextRunAt).
613
+ *
614
+ * @param task - The cron task to evaluate
615
+ * @param now - Current time for comparison
616
+ * @returns true if the task was modified (needs saving)
617
+ */
618
+ async evaluateSingleTask(task, now) {
619
+ if (!task.enabled || !task.nextRunAt)
620
+ return false;
621
+ const nextRun = new Date(task.nextRunAt);
622
+ if (nextRun > now)
623
+ return false;
624
+ // Task is due — check agent status before executing
625
+ let agentOnline = true;
626
+ if (this.agentStatusCallback) {
627
+ try {
628
+ agentOnline = await this.agentStatusCallback(task.targetAgent, task.targetTeamId);
460
629
  }
461
- if (!agentOnline) {
462
- this.logger.warn('Skipping cron task agent offline and auto-start unavailable', {
630
+ catch (error) {
631
+ this.logger.warn('Agent status check failed, assuming offline', {
463
632
  id: task.id,
464
633
  target: task.targetAgent,
634
+ error: error instanceof Error ? error.message : String(error),
465
635
  });
466
- // Still advance nextRunAt to prevent re-evaluating the same missed slot
467
- task.nextRunAt = getNextRunTime(task.cronExpression, task.timezone, now);
468
- updated = true;
469
- continue;
636
+ agentOnline = false;
470
637
  }
471
- this.logger.info('Cron task due, executing', {
638
+ }
639
+ // Auto-start offline agent if callback is available
640
+ if (!agentOnline && this.agentStartCallback) {
641
+ this.logger.info('Agent offline for cron task, attempting auto-start', {
472
642
  id: task.id,
473
643
  target: task.targetAgent,
474
- nextRunAt: task.nextRunAt,
475
644
  });
476
645
  try {
477
- if (this.executionCallback) {
478
- await this.executionCallback(task);
646
+ const started = await this.agentStartCallback(task.targetAgent, task.targetTeamId);
647
+ if (started) {
648
+ agentOnline = true;
649
+ this.logger.info('Agent auto-started successfully', {
650
+ id: task.id,
651
+ target: task.targetAgent,
652
+ });
479
653
  }
480
654
  }
481
655
  catch (error) {
482
- this.logger.error('Cron task execution failed', {
656
+ this.logger.error('Agent auto-start failed', {
483
657
  id: task.id,
658
+ target: task.targetAgent,
484
659
  error: error instanceof Error ? error.message : String(error),
485
660
  });
486
661
  }
487
- // Update run times regardless of execution success
488
- task.lastRunAt = now.toISOString();
662
+ }
663
+ if (!agentOnline) {
664
+ this.logger.warn('Skipping cron task — agent offline and auto-start unavailable', {
665
+ id: task.id,
666
+ target: task.targetAgent,
667
+ });
668
+ // Still advance nextRunAt to prevent re-evaluating the same missed slot
489
669
  task.nextRunAt = getNextRunTime(task.cronExpression, task.timezone, now);
490
- updated = true;
670
+ return true;
671
+ }
672
+ this.logger.info('Cron task due, executing', {
673
+ id: task.id,
674
+ target: task.targetAgent,
675
+ nextRunAt: task.nextRunAt,
676
+ });
677
+ try {
678
+ if (this.executionCallback) {
679
+ await this.executionCallback(task);
680
+ }
681
+ }
682
+ catch (error) {
683
+ this.logger.error('Cron task execution failed', {
684
+ id: task.id,
685
+ error: error instanceof Error ? error.message : String(error),
686
+ });
491
687
  }
492
- if (updated) {
493
- await this.saveStore(store);
688
+ // Update run times regardless of execution success
689
+ task.lastRunAt = now.toISOString();
690
+ task.nextRunAt = getNextRunTime(task.cronExpression, task.timezone, now);
691
+ return true;
692
+ }
693
+ // ============ Storage Methods ============
694
+ /**
695
+ * Load cron task store for a specific team.
696
+ *
697
+ * @param teamId - Team ID
698
+ * @returns Team's cron task store (empty if file doesn't exist)
699
+ */
700
+ async loadTeamStore(teamId) {
701
+ try {
702
+ const filePath = this.getStoreFile(teamId);
703
+ const data = await readFile(filePath, 'utf-8');
704
+ return JSON.parse(data);
494
705
  }
706
+ catch {
707
+ return { tasks: [] };
708
+ }
709
+ }
710
+ /**
711
+ * Save cron task store for a specific team.
712
+ *
713
+ * @param teamId - Team ID
714
+ * @param store - Store data to persist
715
+ */
716
+ async saveTeamStore(teamId, store) {
717
+ const filePath = this.getStoreFile(teamId);
718
+ const dir = path.dirname(filePath);
719
+ await mkdir(dir, { recursive: true });
720
+ await writeFile(filePath, JSON.stringify(store, null, 2), 'utf-8');
495
721
  }
496
722
  /**
497
- * Load the cron task store from disk.
723
+ * Load the global cron task store (orchestrator tasks only).
724
+ *
725
+ * @returns Global store (empty if file doesn't exist)
498
726
  */
499
- async loadStore() {
727
+ async loadGlobalStore() {
500
728
  try {
501
- const data = await readFile(this.storeFile, 'utf-8');
729
+ const data = await readFile(this.getGlobalStoreFile(), 'utf-8');
502
730
  return JSON.parse(data);
503
731
  }
504
732
  catch {
@@ -506,12 +734,61 @@ export class CronTaskService {
506
734
  }
507
735
  }
508
736
  /**
509
- * Save the cron task store to disk.
737
+ * Save the global cron task store (orchestrator tasks only).
738
+ *
739
+ * @param store - Store data to persist
510
740
  */
511
- async saveStore(store) {
512
- const dir = path.dirname(this.storeFile);
741
+ async saveGlobalStore(store) {
742
+ const filePath = this.getGlobalStoreFile();
743
+ const dir = path.dirname(filePath);
513
744
  await mkdir(dir, { recursive: true });
514
- await writeFile(this.storeFile, JSON.stringify(store, null, 2), 'utf-8');
745
+ await writeFile(filePath, JSON.stringify(store, null, 2), 'utf-8');
746
+ }
747
+ /**
748
+ * Load all tasks from all team stores plus the global store,
749
+ * aggregated into a single array.
750
+ *
751
+ * @returns All cron tasks across all teams and the global orchestrator store
752
+ */
753
+ async loadAllTasks() {
754
+ const teamIds = await this.getTeamIds();
755
+ const allTasks = [];
756
+ // Load orchestrator tasks from global store
757
+ const globalStore = await this.loadGlobalStore();
758
+ allTasks.push(...globalStore.tasks);
759
+ // Load per-team tasks
760
+ for (const teamId of teamIds) {
761
+ const store = await this.loadTeamStore(teamId);
762
+ allTasks.push(...store.tasks);
763
+ }
764
+ return allTasks;
765
+ }
766
+ /**
767
+ * Discover all team IDs that have cron task files.
768
+ *
769
+ * Scans ~/.crewly/teams/ for directories containing cron-tasks.json.
770
+ *
771
+ * @returns Array of team IDs
772
+ */
773
+ async getTeamIds() {
774
+ const teamsDir = path.join(this.crewlyHome, 'teams');
775
+ try {
776
+ const entries = await readdir(teamsDir, { withFileTypes: true });
777
+ const teamIds = [];
778
+ for (const entry of entries) {
779
+ if (!entry.isDirectory())
780
+ continue;
781
+ const cronFile = path.join(teamsDir, entry.name, CRON_TASKS_FILENAME);
782
+ if (existsSync(cronFile)) {
783
+ teamIds.push(entry.name);
784
+ }
785
+ }
786
+ return teamIds;
787
+ }
788
+ catch {
789
+ // teams directory doesn't exist yet
790
+ return [];
791
+ }
515
792
  }
516
793
  }
517
794
  //# sourceMappingURL=cron-task.service.js.map