groove-dev 0.27.142 → 0.27.144

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 (187) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +1086 -6532
  4. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
  5. package/node_modules/@groove-dev/daemon/src/index.js +3 -0
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
  7. package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
  8. package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
  9. package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
  10. package/node_modules/@groove-dev/daemon/src/process.js +2 -2
  11. package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
  12. package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
  13. package/node_modules/@groove-dev/daemon/src/routes/agents.js +889 -0
  14. package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
  15. package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
  16. package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
  17. package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
  18. package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
  19. package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
  20. package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
  21. package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
  22. package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
  23. package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
  24. package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
  25. package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
  26. package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
  27. package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
  28. package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
  29. package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
  30. package/node_modules/@groove-dev/gui/dist/assets/index-BcoF6_eF.js +1012 -0
  31. package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.css +1 -0
  32. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  33. package/node_modules/@groove-dev/gui/package.json +1 -1
  34. package/{packages/gui/src/app.jsx → node_modules/@groove-dev/gui/src/App.jsx} +0 -2
  35. package/node_modules/@groove-dev/gui/src/app.css +35 -0
  36. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
  37. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +144 -31
  38. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
  39. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
  40. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
  41. package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
  42. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
  43. package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
  44. package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
  45. package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
  46. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
  47. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
  48. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  49. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
  50. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
  51. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  52. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  53. package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
  54. package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +2 -0
  55. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
  56. package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
  57. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +195 -14
  58. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +286 -102
  59. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
  60. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
  61. package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
  62. package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
  63. package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
  64. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
  65. package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
  66. package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
  67. package/node_modules/@groove-dev/gui/src/lib/status.js +24 -24
  68. package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
  69. package/node_modules/@groove-dev/gui/src/stores/groove.js +34 -3144
  70. package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
  71. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +452 -0
  72. package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
  73. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +227 -0
  74. package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
  75. package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
  76. package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
  77. package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
  78. package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
  79. package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
  80. package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
  81. package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
  82. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
  83. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
  84. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +17 -6
  85. package/node_modules/@groove-dev/gui/src/views/models.jsx +410 -509
  86. package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
  87. package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
  88. package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
  89. package/package.json +1 -1
  90. package/packages/cli/package.json +1 -1
  91. package/packages/daemon/package.json +1 -1
  92. package/packages/daemon/src/api.js +1086 -6532
  93. package/packages/daemon/src/gateways/manager.js +35 -1
  94. package/packages/daemon/src/index.js +3 -0
  95. package/packages/daemon/src/journalist.js +23 -13
  96. package/packages/daemon/src/mlx-server.js +365 -0
  97. package/packages/daemon/src/model-lab.js +308 -12
  98. package/packages/daemon/src/pm.js +1 -1
  99. package/packages/daemon/src/process.js +2 -2
  100. package/packages/daemon/src/providers/local.js +36 -8
  101. package/packages/daemon/src/registry.js +21 -5
  102. package/packages/daemon/src/routes/agents.js +889 -0
  103. package/packages/daemon/src/routes/coordination.js +318 -0
  104. package/packages/daemon/src/routes/files.js +751 -0
  105. package/packages/daemon/src/routes/integrations.js +485 -0
  106. package/packages/daemon/src/routes/network.js +1784 -0
  107. package/packages/daemon/src/routes/providers.js +755 -0
  108. package/packages/daemon/src/routes/schedules.js +110 -0
  109. package/packages/daemon/src/routes/teams.js +650 -0
  110. package/packages/daemon/src/scheduler.js +456 -24
  111. package/packages/daemon/src/teams.js +1 -1
  112. package/packages/daemon/src/validate.js +38 -1
  113. package/packages/daemon/templates/mlx-setup.json +12 -0
  114. package/packages/daemon/templates/tgi-setup.json +1 -1
  115. package/packages/daemon/templates/vllm-setup.json +1 -1
  116. package/packages/gui/dist/assets/index-BcoF6_eF.js +1012 -0
  117. package/packages/gui/dist/assets/index-Dd7qhiEd.css +1 -0
  118. package/packages/gui/dist/index.html +2 -2
  119. package/packages/gui/package.json +1 -1
  120. package/{node_modules/@groove-dev/gui/src/app.jsx → packages/gui/src/App.jsx} +0 -2
  121. package/packages/gui/src/app.css +35 -0
  122. package/packages/gui/src/components/agents/agent-config.jsx +1 -128
  123. package/packages/gui/src/components/agents/agent-feed.jsx +144 -31
  124. package/packages/gui/src/components/agents/agent-node.jsx +8 -13
  125. package/packages/gui/src/components/agents/code-review.jsx +159 -122
  126. package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
  127. package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
  128. package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
  129. package/packages/gui/src/components/automations/automation-card.jsx +274 -0
  130. package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
  131. package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
  132. package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
  133. package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
  134. package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  135. package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
  136. package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
  137. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  138. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  139. package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
  140. package/packages/gui/src/components/editor/selection-menu.jsx +2 -0
  141. package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
  142. package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
  143. package/packages/gui/src/components/lab/parameter-panel.jsx +195 -14
  144. package/packages/gui/src/components/lab/runtime-config.jsx +286 -102
  145. package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
  146. package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
  147. package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
  148. package/packages/gui/src/components/network/network-health.jsx +2 -2
  149. package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
  150. package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
  151. package/packages/gui/src/components/ui/sheet.jsx +5 -2
  152. package/packages/gui/src/lib/cron.js +64 -0
  153. package/packages/gui/src/lib/status.js +24 -24
  154. package/packages/gui/src/lib/theme-hex.js +1 -0
  155. package/packages/gui/src/stores/groove.js +34 -3144
  156. package/packages/gui/src/stores/helpers.js +10 -0
  157. package/packages/gui/src/stores/slices/agents-slice.js +452 -0
  158. package/packages/gui/src/stores/slices/automations-slice.js +96 -0
  159. package/packages/gui/src/stores/slices/chat-slice.js +227 -0
  160. package/packages/gui/src/stores/slices/editor-slice.js +285 -0
  161. package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
  162. package/packages/gui/src/stores/slices/network-slice.js +361 -0
  163. package/packages/gui/src/stores/slices/preview-slice.js +109 -0
  164. package/packages/gui/src/stores/slices/providers-slice.js +897 -0
  165. package/packages/gui/src/stores/slices/teams-slice.js +413 -0
  166. package/packages/gui/src/stores/slices/ui-slice.js +98 -0
  167. package/packages/gui/src/views/agents.jsx +5 -5
  168. package/packages/gui/src/views/dashboard.jsx +12 -13
  169. package/packages/gui/src/views/marketplace.jsx +191 -3
  170. package/packages/gui/src/views/model-lab.jsx +17 -6
  171. package/packages/gui/src/views/models.jsx +410 -509
  172. package/packages/gui/src/views/network.jsx +3 -3
  173. package/packages/gui/src/views/settings.jsx +81 -94
  174. package/packages/gui/src/views/teams.jsx +40 -483
  175. package/SECURITY_SWEEP.md +0 -228
  176. package/TRAINING_DATA_v4.md +0 -6
  177. package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +0 -984
  178. package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +0 -1
  179. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -322
  180. package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
  181. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
  182. package/packages/gui/dist/assets/index-Bjd91ufV.js +0 -984
  183. package/packages/gui/dist/assets/index-BqdwIFn4.css +0 -1
  184. package/packages/gui/src/components/agents/agent-chat.jsx +0 -322
  185. package/packages/gui/src/views/preview.jsx +0 -6
  186. package/packages/gui/src/views/subscription-panel.jsx +0 -327
  187. package/test.py +0 -571
@@ -1,11 +1,11 @@
1
- // GROOVE — Agent Scheduler (Cron-based agent spawning)
1
+ // GROOVE — Agent Scheduler (Cron-based agent spawning + Automations)
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
4
  import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync } from 'fs';
5
5
  import { resolve } from 'path';
6
6
  import { randomUUID } from 'crypto';
7
7
 
8
- // Simple cron field parser — supports: *, N, */N
8
+ // Simple cron field parser — supports: *, N, */N, N,N,N (lists), N-N (ranges)
9
9
  // Fields: minute(0-59) hour(0-23) dayOfMonth(1-31) month(1-12) dayOfWeek(0-6)
10
10
  function parseCronField(field, min, max) {
11
11
  if (field === '*') return null; // any
@@ -14,6 +14,18 @@ function parseCronField(field, min, max) {
14
14
  if (isNaN(step) || step <= 0) return null;
15
15
  return { type: 'step', step };
16
16
  }
17
+ if (field.includes(',')) {
18
+ const values = field.split(',').map((v) => parseInt(v.trim(), 10)).filter((v) => !isNaN(v) && v >= min && v <= max);
19
+ if (values.length === 0) return null;
20
+ return { type: 'list', values };
21
+ }
22
+ if (field.includes('-')) {
23
+ const [startStr, endStr] = field.split('-');
24
+ const start = parseInt(startStr, 10);
25
+ const end = parseInt(endStr, 10);
26
+ if (isNaN(start) || isNaN(end) || start < min || end > max) return null;
27
+ return { type: 'range', start, end };
28
+ }
17
29
  const val = parseInt(field, 10);
18
30
  if (!isNaN(val) && val >= min && val <= max) {
19
31
  return { type: 'exact', value: val };
@@ -25,6 +37,8 @@ function fieldMatches(parsed, value) {
25
37
  if (parsed === null) return true; // wildcard
26
38
  if (parsed.type === 'exact') return value === parsed.value;
27
39
  if (parsed.type === 'step') return value % parsed.step === 0;
40
+ if (parsed.type === 'list') return parsed.values.includes(value);
41
+ if (parsed.type === 'range') return value >= parsed.start && value <= parsed.end;
28
42
  return true;
29
43
  }
30
44
 
@@ -65,10 +79,14 @@ function describeCron(cron) {
65
79
  '0 0 * * 0': 'Weekly (Sunday midnight)',
66
80
  '0 0 * * 1': 'Weekly (Monday midnight)',
67
81
  '0 0 1 * *': 'Monthly (1st at midnight)',
82
+ '0 9,17 * * *': 'Twice daily (9 AM & 5 PM)',
83
+ '0 9 * * 1,4': 'Monday & Thursday at 9 AM',
68
84
  };
69
85
  return presets[cron] || cron;
70
86
  }
71
87
 
88
+ export { describeCron };
89
+
72
90
  const CHECK_INTERVAL = 60_000; // 1 minute
73
91
  const MAX_HISTORY = 50;
74
92
 
@@ -78,7 +96,7 @@ export class Scheduler {
78
96
  this.schedulesDir = resolve(daemon.grooveDir, 'schedules');
79
97
  mkdirSync(this.schedulesDir, { recursive: true });
80
98
  this.schedules = new Map();
81
- this.runningAgents = new Map(); // scheduleId -> agentId
99
+ this.runningAgents = new Map(); // scheduleId -> agentId (or Set of agentIds for teams)
82
100
  this.history = new Map(); // scheduleId -> [{ timestamp, agentId, status }]
83
101
  this.interval = null;
84
102
  this._load();
@@ -90,21 +108,93 @@ export class Scheduler {
90
108
  create(config) {
91
109
  if (!config.name) throw new Error('Schedule name is required');
92
110
  if (!config.cron) throw new Error('Cron expression is required');
93
- if (!config.agentConfig) throw new Error('Agent config is required');
94
- if (!config.agentConfig.role) throw new Error('Agent role is required');
111
+
112
+ // Require either agentConfig or teamConfig
113
+ if (!config.agentConfig && !config.teamConfig) {
114
+ throw new Error('Either agentConfig or teamConfig is required');
115
+ }
116
+ if (config.agentConfig && !config.agentConfig.role) {
117
+ throw new Error('Agent role is required');
118
+ }
119
+ if (config.teamConfig) {
120
+ if (!Array.isArray(config.teamConfig) || config.teamConfig.length === 0) {
121
+ throw new Error('teamConfig must be a non-empty array of agent configs');
122
+ }
123
+ for (const tc of config.teamConfig) {
124
+ if (!tc.role) throw new Error('Each teamConfig entry must have a role');
125
+ }
126
+ }
95
127
 
96
128
  // Validate cron (basic check)
97
129
  const parts = config.cron.trim().split(/\s+/);
98
130
  if (parts.length !== 5) throw new Error('Cron must have 5 fields: minute hour day month weekday');
99
131
 
132
+ // Validate instructionSource
133
+ if (config.instructionSource) {
134
+ const is = config.instructionSource;
135
+ if (!['inline', 'file'].includes(is.type)) {
136
+ throw new Error('instructionSource.type must be "inline" or "file"');
137
+ }
138
+ if (is.type === 'inline' && (!is.content || typeof is.content !== 'string')) {
139
+ throw new Error('instructionSource.content is required for inline type');
140
+ }
141
+ if (is.type === 'file' && (!is.filePath || typeof is.filePath !== 'string')) {
142
+ throw new Error('instructionSource.filePath is required for file type');
143
+ }
144
+ }
145
+
146
+ // Validate outputConfig
147
+ if (config.outputConfig) {
148
+ const oc = config.outputConfig;
149
+ if (oc.gatewayIds && !Array.isArray(oc.gatewayIds)) {
150
+ throw new Error('outputConfig.gatewayIds must be an array');
151
+ }
152
+ if (oc.notifyOn && !['complete', 'error', 'always'].includes(oc.notifyOn)) {
153
+ throw new Error('outputConfig.notifyOn must be "complete", "error", or "always"');
154
+ }
155
+ // Validate gateway IDs reference real gateways
156
+ if (oc.gatewayIds && this.daemon.gateways) {
157
+ for (const gid of oc.gatewayIds) {
158
+ if (!this.daemon.gateways.get(gid)) {
159
+ throw new Error(`Gateway not found: ${gid}`);
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ // Validate integrationIds
166
+ if (config.integrationIds) {
167
+ if (!Array.isArray(config.integrationIds)) {
168
+ throw new Error('integrationIds must be an array');
169
+ }
170
+ if (this.daemon.integrations) {
171
+ const installed = this.daemon.integrations.getInstalled();
172
+ for (const iid of config.integrationIds) {
173
+ if (!installed.some((i) => i.id === iid)) {
174
+ throw new Error(`Integration not installed: ${iid}`);
175
+ }
176
+ }
177
+ }
178
+ }
179
+
100
180
  const schedule = {
101
181
  id: randomUUID().slice(0, 8),
102
182
  name: config.name,
103
183
  cron: config.cron.trim(),
104
184
  cronDescription: describeCron(config.cron.trim()),
105
- agentConfig: config.agentConfig,
185
+ agentConfig: config.agentConfig || null,
186
+ teamConfig: config.teamConfig || null,
187
+ instructionSource: config.instructionSource || null,
188
+ outputConfig: config.outputConfig || null,
189
+ integrationIds: config.integrationIds || null,
190
+ description: config.description || '',
191
+ teamName: config.teamName || '',
106
192
  enabled: config.enabled !== false,
107
193
  maxConcurrent: config.maxConcurrent || 1,
194
+ lastRunStatus: null,
195
+ lastRunAt: null,
196
+ lastRunDuration: null,
197
+ lastRunCost: null,
108
198
  createdAt: new Date().toISOString(),
109
199
  updatedAt: new Date().toISOString(),
110
200
  };
@@ -113,6 +203,7 @@ export class Scheduler {
113
203
  this.history.set(schedule.id, []);
114
204
  this._save(schedule.id);
115
205
 
206
+ this.daemon.broadcast({ type: 'schedule:created', data: schedule });
116
207
  this.daemon.audit.log('schedule.create', { id: schedule.id, name: schedule.name, cron: schedule.cron });
117
208
 
118
209
  return schedule;
@@ -125,7 +216,11 @@ export class Scheduler {
125
216
  const schedule = this.schedules.get(id);
126
217
  if (!schedule) throw new Error(`Schedule not found: ${id}`);
127
218
 
128
- const SAFE = ['name', 'cron', 'agentConfig', 'enabled', 'maxConcurrent'];
219
+ const SAFE = [
220
+ 'name', 'cron', 'agentConfig', 'teamConfig', 'instructionSource',
221
+ 'outputConfig', 'integrationIds', 'description', 'teamName',
222
+ 'enabled', 'maxConcurrent',
223
+ ];
129
224
  for (const key of Object.keys(updates)) {
130
225
  if (SAFE.includes(key)) {
131
226
  schedule[key] = updates[key];
@@ -137,6 +232,7 @@ export class Scheduler {
137
232
  schedule.updatedAt = new Date().toISOString();
138
233
  this._save(id);
139
234
 
235
+ this.daemon.broadcast({ type: 'schedule:updated', data: schedule });
140
236
  return schedule;
141
237
  }
142
238
 
@@ -152,6 +248,7 @@ export class Scheduler {
152
248
  const filePath = resolve(this.schedulesDir, `${id}.json`);
153
249
  if (existsSync(filePath)) unlinkSync(filePath);
154
250
 
251
+ this.daemon.broadcast({ type: 'schedule:deleted', data: { id } });
155
252
  this.daemon.audit.log('schedule.delete', { id });
156
253
  }
157
254
 
@@ -164,6 +261,7 @@ export class Scheduler {
164
261
  schedule.enabled = true;
165
262
  schedule.updatedAt = new Date().toISOString();
166
263
  this._save(id);
264
+ this.daemon.broadcast({ type: 'schedule:updated', data: schedule });
167
265
  return schedule;
168
266
  }
169
267
 
@@ -176,6 +274,7 @@ export class Scheduler {
176
274
  schedule.enabled = false;
177
275
  schedule.updatedAt = new Date().toISOString();
178
276
  this._save(id);
277
+ this.daemon.broadcast({ type: 'schedule:updated', data: schedule });
179
278
  return schedule;
180
279
  }
181
280
 
@@ -183,11 +282,20 @@ export class Scheduler {
183
282
  * List all schedules with their current state.
184
283
  */
185
284
  list() {
186
- return Array.from(this.schedules.values()).map((s) => ({
187
- ...s,
188
- lastRun: this._lastRun(s.id),
189
- isRunning: this.runningAgents.has(s.id),
190
- }));
285
+ return Array.from(this.schedules.values()).map((s) => {
286
+ const runInfo = this.runningAgents.get(s.id);
287
+ let activeAgentIds = null;
288
+ if (runInfo) {
289
+ if (typeof runInfo === 'string') activeAgentIds = [runInfo];
290
+ else if (runInfo.agentIds) activeAgentIds = runInfo.agentIds;
291
+ }
292
+ return {
293
+ ...s,
294
+ lastRun: this._lastRun(s.id),
295
+ isRunning: this.runningAgents.has(s.id),
296
+ activeAgentIds,
297
+ };
298
+ });
191
299
  }
192
300
 
193
301
  /**
@@ -196,14 +304,48 @@ export class Scheduler {
196
304
  get(id) {
197
305
  const schedule = this.schedules.get(id);
198
306
  if (!schedule) return null;
307
+ const runInfo = this.runningAgents.get(id);
308
+ let activeAgentIds = null;
309
+ if (runInfo) {
310
+ if (typeof runInfo === 'string') activeAgentIds = [runInfo];
311
+ else if (runInfo.agentIds) activeAgentIds = runInfo.agentIds;
312
+ }
199
313
  return {
200
314
  ...schedule,
201
315
  history: this.history.get(id) || [],
202
316
  lastRun: this._lastRun(id),
203
317
  isRunning: this.runningAgents.has(id),
318
+ activeAgentIds,
204
319
  };
205
320
  }
206
321
 
322
+ /**
323
+ * Get run history for a schedule.
324
+ */
325
+ getRunHistory(id) {
326
+ if (!this.schedules.has(id)) return null;
327
+ return this.history.get(id) || [];
328
+ }
329
+
330
+ /**
331
+ * Duplicate a schedule with a new ID.
332
+ */
333
+ duplicate(id) {
334
+ const original = this.schedules.get(id);
335
+ if (!original) throw new Error(`Schedule not found: ${id}`);
336
+
337
+ const clone = {
338
+ ...original,
339
+ name: `${original.name} (Copy)`,
340
+ lastRunStatus: null,
341
+ lastRunAt: null,
342
+ lastRunDuration: null,
343
+ lastRunCost: null,
344
+ };
345
+ // create() will assign a new ID and timestamps
346
+ return this.create(clone);
347
+ }
348
+
207
349
  /**
208
350
  * Manually trigger a schedule (run now).
209
351
  */
@@ -240,14 +382,25 @@ export class Scheduler {
240
382
  if (cronMatches(schedule.cron, now)) {
241
383
  // Check concurrency
242
384
  if (this.runningAgents.has(schedule.id)) {
243
- const agentId = this.runningAgents.get(schedule.id);
244
- const agent = this.daemon.registry.get(agentId);
245
- if (agent && (agent.status === 'running' || agent.status === 'starting')) {
246
- // Still running skip
247
- this._recordHistory(schedule.id, null, 'skipped');
248
- continue;
385
+ const runInfo = this.runningAgents.get(schedule.id);
386
+ // For team runs, runInfo is { agentIds, teamId, startedAt }
387
+ // For single agent runs, it's an agentId string (backward compat)
388
+ if (typeof runInfo === 'string') {
389
+ const agent = this.daemon.registry.get(runInfo);
390
+ if (agent && (agent.status === 'running' || agent.status === 'starting')) {
391
+ this._recordHistory(schedule.id, null, 'skipped');
392
+ continue;
393
+ }
394
+ } else if (runInfo && runInfo.agentIds) {
395
+ const anyRunning = runInfo.agentIds.some((aid) => {
396
+ const a = this.daemon.registry.get(aid);
397
+ return a && (a.status === 'running' || a.status === 'starting');
398
+ });
399
+ if (anyRunning) {
400
+ this._recordHistory(schedule.id, null, 'skipped');
401
+ continue;
402
+ }
249
403
  }
250
- // Agent finished — clear
251
404
  this.runningAgents.delete(schedule.id);
252
405
  }
253
406
  this._execute(schedule).catch(() => {});
@@ -255,12 +408,74 @@ export class Scheduler {
255
408
  }
256
409
  }
257
410
 
411
+ /**
412
+ * Resolve the instruction prompt for this schedule.
413
+ */
414
+ _resolveInstruction(schedule) {
415
+ let instruction = null;
416
+ if (schedule.instructionSource) {
417
+ const is = schedule.instructionSource;
418
+ if (is.type === 'inline') instruction = is.content;
419
+ if (is.type === 'file') {
420
+ if (!existsSync(is.filePath)) {
421
+ throw new Error(`Instruction file not found: ${is.filePath}`);
422
+ }
423
+ instruction = readFileSync(is.filePath, 'utf8');
424
+ }
425
+ }
426
+
427
+ const oc = schedule.outputConfig;
428
+ if (oc) {
429
+ const parts = [];
430
+ if (oc.filePath) parts.push(`Save output/results to: ${oc.filePath}`);
431
+ if (oc.customInstructions) parts.push(oc.customInstructions);
432
+ if (parts.length > 0) {
433
+ const section = '\n\n## Output\n' + parts.join('\n');
434
+ instruction = instruction ? instruction + section : section;
435
+ }
436
+ }
437
+
438
+ return instruction;
439
+ }
440
+
258
441
  async _execute(schedule) {
442
+ const startedAt = Date.now();
443
+
444
+ // Update run status
445
+ schedule.lastRunStatus = 'running';
446
+ schedule.lastRunAt = new Date().toISOString();
447
+ this._save(schedule.id);
448
+
449
+ // Resolve instruction prompt
450
+ let instruction;
259
451
  try {
260
- const agent = await this.daemon.processes.spawn({
261
- ...schedule.agentConfig,
262
- name: `sched-${schedule.name.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 20)}`,
263
- });
452
+ instruction = this._resolveInstruction(schedule);
453
+ } catch (err) {
454
+ this._recordHistory(schedule.id, null, 'error', err.message);
455
+ schedule.lastRunStatus = 'error';
456
+ schedule.lastRunDuration = Date.now() - startedAt;
457
+ this._save(schedule.id);
458
+ throw err;
459
+ }
460
+
461
+ // Team-based execution
462
+ if (schedule.teamConfig) {
463
+ return this._executeTeam(schedule, instruction, startedAt);
464
+ }
465
+
466
+ // Single agent execution (backward compatible)
467
+ return this._executeSingle(schedule, instruction, startedAt);
468
+ }
469
+
470
+ async _executeSingle(schedule, instruction, startedAt) {
471
+ try {
472
+ const config = { ...schedule.agentConfig };
473
+ if (instruction) config.prompt = instruction;
474
+ if (schedule.integrationIds) config.integrations = schedule.integrationIds;
475
+ config.name = `sched-${schedule.name.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 20)}`;
476
+ config.metadata = { ...config.metadata, scheduled: true, scheduleId: schedule.id };
477
+
478
+ const agent = await this.daemon.processes.spawn(config);
264
479
  this.runningAgents.set(schedule.id, agent.id);
265
480
  this._recordHistory(schedule.id, agent.id, 'spawned');
266
481
 
@@ -276,13 +491,220 @@ export class Scheduler {
276
491
  agentId: agent.id,
277
492
  });
278
493
 
494
+ // Watch for completion to update run metadata and send notifications
495
+ this._watchAgent(schedule, agent.id, startedAt);
496
+
279
497
  return agent;
280
498
  } catch (err) {
281
499
  this._recordHistory(schedule.id, null, 'error', err.message);
500
+ schedule.lastRunStatus = 'error';
501
+ schedule.lastRunDuration = Date.now() - startedAt;
502
+ this._save(schedule.id);
503
+ this._sendOutputNotification(schedule, 'error', startedAt, err.message);
504
+ throw err;
505
+ }
506
+ }
507
+
508
+ async _executeTeam(schedule, instruction, startedAt) {
509
+ try {
510
+ // Create a team for this run
511
+ const teamName = schedule.teamName || schedule.name;
512
+ let team;
513
+ try {
514
+ team = this.daemon.teams.create(teamName);
515
+ } catch {
516
+ // Team name might already exist, use a unique suffix
517
+ team = this.daemon.teams.create(`${teamName}-${Date.now().toString(36)}`);
518
+ }
519
+
520
+ const defaultProvider = this.daemon.config?.defaultProvider || 'claude-code';
521
+ const defaultDir = this.daemon.config?.defaultWorkingDir || this.daemon.projectDir;
522
+
523
+ // Separate phases
524
+ const phase1Configs = schedule.teamConfig.filter((c) => !c.phase || c.phase === 1);
525
+ const phase2Configs = schedule.teamConfig.filter((c) => c.phase === 2);
526
+
527
+ const allAgentIds = [];
528
+ const phase1Ids = [];
529
+
530
+ // Spawn phase 1 agents
531
+ for (const tc of phase1Configs) {
532
+ const config = {
533
+ role: tc.role,
534
+ scope: tc.scope || [],
535
+ provider: tc.provider || defaultProvider,
536
+ model: tc.model || 'auto',
537
+ permission: tc.permission || 'auto',
538
+ workingDir: defaultDir,
539
+ name: tc.name || undefined,
540
+ };
541
+ if (instruction) config.prompt = instruction;
542
+ else if (tc.prompt) config.prompt = tc.prompt;
543
+ if (schedule.integrationIds) config.integrations = schedule.integrationIds;
544
+ config.teamId = team.id;
545
+ config.metadata = { scheduled: true, scheduleId: schedule.id };
546
+
547
+ try {
548
+ const agent = await this.daemon.processes.spawn(config);
549
+ phase1Ids.push(agent.id);
550
+ allAgentIds.push(agent.id);
551
+ } catch (err) {
552
+ console.log(`[Groove:Scheduler] Failed to spawn ${tc.role}: ${err.message}`);
553
+ }
554
+ }
555
+
556
+ if (phase1Ids.length === 0) {
557
+ throw new Error('Failed to spawn any phase 1 agents');
558
+ }
559
+
560
+ // Register phase 2 for auto-spawn
561
+ if (phase2Configs.length > 0) {
562
+ this.daemon._pendingPhase2 = this.daemon._pendingPhase2 || [];
563
+ this.daemon._pendingPhase2.push({
564
+ waitFor: phase1Ids,
565
+ agents: phase2Configs.map((c) => ({
566
+ role: c.role,
567
+ scope: c.scope || [],
568
+ prompt: c.prompt || (instruction ? instruction : ''),
569
+ provider: c.provider || defaultProvider,
570
+ model: c.model || 'auto',
571
+ permission: c.permission || 'auto',
572
+ workingDir: defaultDir,
573
+ name: c.name || undefined,
574
+ teamId: team.id,
575
+ })),
576
+ });
577
+ }
578
+
579
+ this.runningAgents.set(schedule.id, {
580
+ agentIds: allAgentIds,
581
+ teamId: team.id,
582
+ startedAt,
583
+ });
584
+
585
+ this._recordHistory(schedule.id, phase1Ids.join(','), 'spawned');
586
+
587
+ this.daemon.broadcast({
588
+ type: 'schedule:execute',
589
+ scheduleId: schedule.id,
590
+ teamId: team.id,
591
+ agentCount: phase1Ids.length,
592
+ });
593
+
594
+ this.daemon.audit.log('schedule.executeTeam', {
595
+ id: schedule.id,
596
+ name: schedule.name,
597
+ teamId: team.id,
598
+ phase1: phase1Ids.length,
599
+ phase2Pending: phase2Configs.length,
600
+ });
601
+
602
+ // Watch all agents for completion
603
+ for (const aid of allAgentIds) {
604
+ this._watchAgent(schedule, aid, startedAt);
605
+ }
606
+
607
+ return { teamId: team.id, agentIds: allAgentIds };
608
+ } catch (err) {
609
+ this._recordHistory(schedule.id, null, 'error', err.message);
610
+ schedule.lastRunStatus = 'error';
611
+ schedule.lastRunDuration = Date.now() - startedAt;
612
+ this._save(schedule.id);
613
+ this._sendOutputNotification(schedule, 'error', startedAt, err.message);
282
614
  throw err;
283
615
  }
284
616
  }
285
617
 
618
+ /**
619
+ * Watch an agent for completion and update run metadata.
620
+ */
621
+ _watchAgent(schedule, agentId, startedAt) {
622
+ const checkInterval = setInterval(() => {
623
+ const agent = this.daemon.registry.get(agentId);
624
+ if (!agent) {
625
+ clearInterval(checkInterval);
626
+ return;
627
+ }
628
+
629
+ const terminal = new Set(['completed', 'crashed', 'stopped', 'killed']);
630
+ if (!terminal.has(agent.status)) return;
631
+
632
+ clearInterval(checkInterval);
633
+
634
+ const runInfo = this.runningAgents.get(schedule.id);
635
+
636
+ // For team runs, check if ALL agents are done
637
+ if (runInfo && typeof runInfo === 'object' && runInfo.agentIds) {
638
+ const allDone = runInfo.agentIds.every((aid) => {
639
+ const a = this.daemon.registry.get(aid);
640
+ return !a || terminal.has(a.status);
641
+ });
642
+ if (!allDone) return;
643
+
644
+ // Also check for phase 2 agents that were spawned after
645
+ const teamAgents = this.daemon.registry.getAll().filter((a) => a.teamId === runInfo.teamId);
646
+ const teamAllDone = teamAgents.every((a) => terminal.has(a.status));
647
+ if (!teamAllDone) return;
648
+
649
+ // All team agents done
650
+ const anyError = teamAgents.some((a) => a.status === 'crashed');
651
+ const totalCost = teamAgents.reduce((sum, a) => sum + (a.costUsd || 0), 0);
652
+
653
+ schedule.lastRunStatus = anyError ? 'error' : 'success';
654
+ schedule.lastRunDuration = Date.now() - startedAt;
655
+ schedule.lastRunCost = totalCost;
656
+ this._save(schedule.id);
657
+ this.runningAgents.delete(schedule.id);
658
+
659
+ this._recordHistory(schedule.id, runInfo.agentIds.join(','), anyError ? 'error' : 'completed');
660
+ this._sendOutputNotification(schedule, anyError ? 'error' : 'success', startedAt,
661
+ anyError ? `${teamAgents.filter((a) => a.status === 'crashed').length} agent(s) crashed` : null);
662
+ return;
663
+ }
664
+
665
+ // Single agent completion
666
+ schedule.lastRunStatus = agent.status === 'completed' ? 'success' : 'error';
667
+ schedule.lastRunDuration = Date.now() - startedAt;
668
+ schedule.lastRunCost = agent.costUsd || 0;
669
+ this._save(schedule.id);
670
+ this.runningAgents.delete(schedule.id);
671
+
672
+ this._recordHistory(schedule.id, agentId, agent.status === 'completed' ? 'completed' : 'error');
673
+ this._sendOutputNotification(schedule,
674
+ agent.status === 'completed' ? 'success' : 'error',
675
+ startedAt,
676
+ agent.status !== 'completed' ? `Agent ${agent.status}` : null);
677
+ }, 5000);
678
+ }
679
+
680
+ /**
681
+ * Send output notification through configured gateways.
682
+ */
683
+ _sendOutputNotification(schedule, status, startedAt, errorDetails) {
684
+ if (!schedule.outputConfig || !schedule.outputConfig.gatewayIds || schedule.outputConfig.gatewayIds.length === 0) return;
685
+
686
+ const notifyOn = schedule.outputConfig.notifyOn || 'always';
687
+ if (notifyOn === 'complete' && status !== 'success') return;
688
+ if (notifyOn === 'error' && status !== 'error') return;
689
+
690
+ const duration = Date.now() - startedAt;
691
+ const agentCount = schedule.teamConfig ? schedule.teamConfig.length : 1;
692
+
693
+ const summary = {
694
+ name: schedule.name,
695
+ description: schedule.description || '',
696
+ status,
697
+ duration,
698
+ cost: schedule.lastRunCost || 0,
699
+ agentCount,
700
+ errors: errorDetails || null,
701
+ };
702
+
703
+ if (this.daemon.gateways && typeof this.daemon.gateways.sendScheduleNotification === 'function') {
704
+ this.daemon.gateways.sendScheduleNotification(schedule.outputConfig.gatewayIds, summary);
705
+ }
706
+ }
707
+
286
708
  _recordHistory(scheduleId, agentId, status, error) {
287
709
  const history = this.history.get(scheduleId) || [];
288
710
  history.unshift({
@@ -323,9 +745,19 @@ export class Scheduler {
323
745
  name: data.name,
324
746
  cron: data.cron,
325
747
  cronDescription: describeCron(data.cron),
326
- agentConfig: data.agentConfig,
748
+ agentConfig: data.agentConfig || null,
749
+ teamConfig: data.teamConfig || null,
750
+ instructionSource: data.instructionSource || null,
751
+ outputConfig: data.outputConfig || null,
752
+ integrationIds: data.integrationIds || null,
753
+ description: data.description || '',
754
+ teamName: data.teamName || '',
327
755
  enabled: data.enabled !== false,
328
756
  maxConcurrent: data.maxConcurrent || 1,
757
+ lastRunStatus: data.lastRunStatus || null,
758
+ lastRunAt: data.lastRunAt || null,
759
+ lastRunDuration: data.lastRunDuration || null,
760
+ lastRunCost: data.lastRunCost || null,
329
761
  createdAt: data.createdAt,
330
762
  updatedAt: data.updatedAt,
331
763
  });
@@ -371,7 +371,7 @@ export class Teams {
371
371
  const defaultTeam = this.getDefault();
372
372
  if (!defaultTeam) return;
373
373
  for (const agent of this.daemon.registry.getAll()) {
374
- if (!agent.teamId) {
374
+ if (!agent.teamId && !agent.metadata?.scheduled) {
375
375
  this.daemon.registry.update(agent.id, { teamId: defaultTeam.id });
376
376
  }
377
377
  }