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
@@ -0,0 +1,650 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { resolve, relative } from 'path';
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
5
+ import { homedir } from 'os';
6
+ import { validateAgentConfig, validateTeamMode } from '../validate.js';
7
+
8
+ export function registerTeamRoutes(app, daemon) {
9
+
10
+ // --- Teams (live agent groups) ---
11
+
12
+ app.get('/api/teams', (req, res) => {
13
+ res.json({
14
+ teams: daemon.teams.list(),
15
+ defaultTeamId: daemon.teams.getDefault()?.id || null,
16
+ });
17
+ });
18
+
19
+ app.post('/api/teams', (req, res) => {
20
+ try {
21
+ const team = daemon.teams.create(req.body.name, { mode: req.body.mode });
22
+ daemon.audit.log('team.create', { id: team.id, name: team.name, mode: team.mode, workingDir: team.workingDir });
23
+ res.status(201).json(team);
24
+ } catch (err) {
25
+ res.status(400).json({ error: err.message });
26
+ }
27
+ });
28
+
29
+ app.get('/api/teams/archived', (req, res) => {
30
+ res.json({ archived: daemon.teams.listArchived() });
31
+ });
32
+
33
+ app.post('/api/teams/archived/:id/restore', (req, res) => {
34
+ try {
35
+ const team = daemon.teams.restore(req.params.id);
36
+ daemon.audit.log('team.restore', { archivedId: req.params.id, newId: team.id, name: team.name });
37
+ res.json(team);
38
+ } catch (err) {
39
+ res.status(400).json({ error: err.message });
40
+ }
41
+ });
42
+
43
+ app.delete('/api/teams/archived/:id', (req, res) => {
44
+ try {
45
+ daemon.teams.purge(req.params.id);
46
+ daemon.audit.log('team.purge', { archivedId: req.params.id });
47
+ res.json({ ok: true });
48
+ } catch (err) {
49
+ res.status(400).json({ error: err.message });
50
+ }
51
+ });
52
+
53
+ app.patch('/api/teams/:id', (req, res) => {
54
+ try {
55
+ if (req.body.name) daemon.teams.rename(req.params.id, req.body.name);
56
+ if (req.body.workingDir !== undefined) daemon.teams.setWorkingDir(req.params.id, req.body.workingDir);
57
+ const team = daemon.teams.get(req.params.id);
58
+ daemon.audit.log('team.update', { id: team.id, name: team.name, workingDir: team.workingDir });
59
+ res.json(team);
60
+ } catch (err) {
61
+ res.status(400).json({ error: err.message });
62
+ }
63
+ });
64
+
65
+ app.delete('/api/teams/:id', (req, res) => {
66
+ try {
67
+ const permanent = req.query.permanent === 'true';
68
+ daemon.teams.delete(req.params.id, { permanent });
69
+ daemon.audit.log(permanent ? 'team.delete' : 'team.archive', { id: req.params.id, permanent });
70
+ res.json({ ok: true });
71
+ } catch (err) {
72
+ res.status(400).json({ error: err.message });
73
+ }
74
+ });
75
+
76
+ app.post('/api/teams/:id/promote', (req, res) => {
77
+ try {
78
+ const result = daemon.teams.promote(req.params.id);
79
+ daemon.audit.log('team.promote', { id: req.params.id, destination: result.destination });
80
+ res.json(result);
81
+ } catch (err) {
82
+ res.status(400).json({ error: err.message });
83
+ }
84
+ });
85
+
86
+ // --- Project Manager (AI Review Gate) ---
87
+
88
+ // Agent knocks on PM before risky operations (Auto permission mode)
89
+ app.post('/api/pm/review', async (req, res) => {
90
+ try {
91
+ const { agent, action, file, description } = req.body;
92
+ if (!agent || !action || !file) {
93
+ return res.status(400).json({ error: 'agent, action, and file are required' });
94
+ }
95
+ const result = await daemon.pm.review({ agent, action, file, description: description || '' });
96
+ res.json(result);
97
+ } catch (err) {
98
+ // On failure, approve by default (don't block agents)
99
+ res.json({ approved: true, reason: `PM error: ${err.message}. Auto-approved.` });
100
+ }
101
+ });
102
+
103
+ // PM review history for Approvals tab
104
+ app.get('/api/pm/history', (req, res) => {
105
+ res.json({
106
+ history: daemon.pm.getHistory(),
107
+ stats: daemon.pm.getStats(),
108
+ });
109
+ });
110
+
111
+ // --- Recommended Team (from planner) ---
112
+
113
+ // Find recommended-team.json — check planner agents first (they write the file),
114
+ // sorted by most recent activity so the latest planner's team wins.
115
+ function findRecommendedTeam() {
116
+ const agents = daemon.registry.getAll();
117
+ const planners = agents
118
+ .filter((a) => a.role === 'planner' && a.workingDir)
119
+ .sort((a, b) => (b.lastActivity || b.spawnedAt || '').localeCompare(a.lastActivity || a.spawnedAt || ''));
120
+
121
+ const candidates = [];
122
+ for (const planner of planners) {
123
+ const p = resolve(planner.workingDir, '.groove', 'recommended-team.json');
124
+ if (existsSync(p)) candidates.push({ path: p, teamId: planner.teamId || null, agentId: planner.id || null });
125
+ }
126
+ const fallback = resolve(daemon.grooveDir, 'recommended-team.json');
127
+ if (existsSync(fallback) && !candidates.some(c => c.path === fallback)) {
128
+ candidates.push({ path: fallback, teamId: planners[0]?.teamId || null, agentId: planners[0]?.id || null });
129
+ }
130
+ if (candidates.length === 0) return null;
131
+
132
+ for (const c of candidates) {
133
+ try {
134
+ const data = JSON.parse(readFileSync(c.path, 'utf8'));
135
+ if (data._meta?.teamId) {
136
+ return { path: c.path, teamId: data._meta.teamId, agentId: data._meta.agentId || c.agentId };
137
+ }
138
+ } catch {}
139
+ }
140
+ return candidates[0];
141
+ }
142
+
143
+ app.get('/api/recommended-team', (req, res) => {
144
+ const found = findRecommendedTeam();
145
+ if (!found) {
146
+ return res.json({ exists: false, agents: [] });
147
+ }
148
+ try {
149
+ const raw = JSON.parse(readFileSync(found.path, 'utf8'));
150
+ delete raw._meta;
151
+ // Support both old format (bare array) and new format ({ projectDir, agents })
152
+ if (Array.isArray(raw)) {
153
+ res.json({ exists: true, agents: raw, teamId: found.teamId });
154
+ } else if (raw && Array.isArray(raw.agents)) {
155
+ res.json({ exists: true, agents: raw.agents, projectDir: raw.projectDir || null, teamId: found.teamId });
156
+ } else {
157
+ res.json({ exists: false, agents: [] });
158
+ }
159
+ } catch {
160
+ res.json({ exists: false, agents: [] });
161
+ }
162
+ });
163
+
164
+ app.post('/api/recommended-team/launch', async (req, res) => {
165
+ const found = findRecommendedTeam();
166
+ if (!found) {
167
+ return res.status(404).json({ error: 'No recommended team found. Run a planner first.' });
168
+ }
169
+ const planPath = found.path;
170
+ const planContents = readFileSync(planPath, 'utf8');
171
+ try {
172
+ const raw = JSON.parse(planContents);
173
+ delete raw._meta;
174
+
175
+ // Delete immediately after reading to prevent duplicate launches from poll races.
176
+ // If every spawn below fails, we'll restore the plan from planContents so the
177
+ // user can retry without re-prompting the planner.
178
+ try { unlinkSync(planPath); } catch { /* already gone */ }
179
+
180
+ // Support both old format (bare array) and new format ({ projectDir, agents, preview })
181
+ let agentConfigs;
182
+ let projectDir = null;
183
+ let previewBlock = null;
184
+
185
+ // Frontend Team Builder override — if body.agents is provided, use it
186
+ // instead of the planner's recommended-team.json
187
+ if (Array.isArray(req.body?.agents) && req.body.agents.length > 0) {
188
+ agentConfigs = req.body.agents;
189
+ projectDir = raw.projectDir || null;
190
+ previewBlock = raw.preview || null;
191
+ } else if (Array.isArray(raw)) {
192
+ agentConfigs = raw;
193
+ } else if (raw && Array.isArray(raw.agents)) {
194
+ agentConfigs = raw.agents;
195
+ projectDir = raw.projectDir || null;
196
+ previewBlock = raw.preview || null;
197
+ } else {
198
+ return res.status(400).json({ error: 'Invalid recommended team format' });
199
+ }
200
+
201
+ if (agentConfigs.length === 0) {
202
+ return res.status(400).json({ error: 'Recommended team is empty' });
203
+ }
204
+
205
+ const maxPhase = agentConfigs.reduce((max, config) => {
206
+ const phase = typeof config.phase === 'number' ? config.phase : 1;
207
+ return Math.max(max, phase);
208
+ }, 1);
209
+
210
+ // Resolve base directory from the planner that wrote the file, not the daemon root
211
+ const plannerAgent = found.agentId ? daemon.registry.get(found.agentId) : null;
212
+ const baseDir = plannerAgent?.workingDir || daemon.config?.defaultWorkingDir || daemon.projectDir;
213
+ const plannerProvider = plannerAgent?.provider || undefined;
214
+ const plannerModel = plannerAgent?.model || undefined;
215
+
216
+ // Use the planner's teamId so launched agents join the correct team.
217
+ // Priority: explicit from frontend > agent that wrote the file > most recent planner > default
218
+ let launchTeamId = req.body?.teamId || found.teamId || null;
219
+ if (!launchTeamId) {
220
+ const planners = daemon.registry.getAll()
221
+ .filter((a) => a.role === 'planner')
222
+ .sort((a, b) => (b.lastActivity || '').localeCompare(a.lastActivity || ''));
223
+ launchTeamId = planners[0]?.teamId || null;
224
+ }
225
+ const defaultTeamId = launchTeamId || daemon.teams.getDefault()?.id || null;
226
+
227
+ // Determine team build mode
228
+ let launchMode;
229
+ try { launchMode = validateTeamMode(req.body?.mode || raw.mode); } catch { launchMode = 'sandbox'; }
230
+
231
+ // If planner specified a project directory, create it and use it as workingDir
232
+ // Production mode: always use projectDir directly, skip subdirectory creation
233
+ let projectWorkingDir = baseDir;
234
+ if (launchMode === 'production') {
235
+ projectWorkingDir = daemon.projectDir;
236
+ console.log(`[Groove] Production mode — working in project root: ${projectWorkingDir}`);
237
+ } else if (projectDir) {
238
+ // Sanitize: kebab-case, no path traversal
239
+ const safeName = String(projectDir).replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64);
240
+ projectWorkingDir = resolve(baseDir, safeName);
241
+ mkdirSync(projectWorkingDir, { recursive: true });
242
+ console.log(`[Groove] Project directory: ${projectWorkingDir}`);
243
+ }
244
+
245
+ function normalizeScope(patterns, baseDir) {
246
+ if (!patterns || !Array.isArray(patterns)) return patterns;
247
+ return patterns.map((p) => {
248
+ if (typeof p === 'string' && p.startsWith('/')) {
249
+ const rel = relative(baseDir, p);
250
+ if (!rel.startsWith('..')) return rel;
251
+ return p.slice(1);
252
+ }
253
+ return p;
254
+ });
255
+ }
256
+
257
+ function resolveProviderAndModel(cfgProvider, cfgModel, fallbackProvider, fallbackModel) {
258
+ const provider = cfgProvider || plannerProvider || daemon.config?.defaultProvider || fallbackProvider || undefined;
259
+ if (cfgModel) return { provider, model: cfgModel };
260
+ if (!cfgProvider && plannerProvider && plannerProvider !== daemon.config?.defaultProvider) {
261
+ return { provider, model: plannerModel || 'auto' };
262
+ }
263
+ return { provider, model: daemon.config?.defaultModel || fallbackModel || 'auto' };
264
+ }
265
+
266
+ // Team-level overrides from the pre-planner config panel
267
+ const teamProvider = req.body?.teamProvider || undefined;
268
+ const teamModel = req.body?.teamModel || undefined;
269
+ const teamReasoningEffort = req.body?.teamReasoningEffort != null ? Number(req.body.teamReasoningEffort) : undefined;
270
+ const teamTemperature = req.body?.teamTemperature != null ? Number(req.body.teamTemperature) : undefined;
271
+ const teamVerbosity = req.body?.teamVerbosity != null ? Number(req.body.teamVerbosity) : undefined;
272
+
273
+ if (teamProvider || teamModel) {
274
+ for (const c of agentConfigs) {
275
+ if (teamProvider) c.provider = teamProvider;
276
+ if (teamModel) c.model = teamModel;
277
+ if (teamReasoningEffort !== undefined) c.reasoningEffort = teamReasoningEffort;
278
+ if (teamTemperature !== undefined) c.temperature = teamTemperature;
279
+ if (teamVerbosity !== undefined) c.verbosity = teamVerbosity;
280
+ }
281
+ }
282
+
283
+ // Separate phase 1 (builders) and phase 2 (QC/finisher)
284
+ const phase1 = agentConfigs.filter((a) => !a.phase || a.phase === 1);
285
+ let phase2 = agentConfigs.filter((a) => a.phase === 2);
286
+
287
+ // Safety net: if planner forgot the QC agent, auto-add one
288
+ if (phase2.length === 0 && phase1.length >= 2) {
289
+ const { provider: qcProvider, model: qcModel } = resolveProviderAndModel(teamProvider, teamModel);
290
+ phase2 = [{
291
+ name: 'qc-agent',
292
+ role: 'fullstack', phase: 2, scope: [],
293
+ provider: qcProvider,
294
+ model: qcModel,
295
+ prompt: 'QC Senior Dev: All builder agents have completed. Audit their changes for correctness, fix any issues, run tests, and verify the project builds cleanly (npm run build). Do NOT start long-running dev servers — just verify the build succeeds. Commit all changes. IMPORTANT: Do NOT delete files from other projects or directories outside this project.',
296
+ }];
297
+ }
298
+
299
+ // Reset handoff cycle counters for this team so a fresh launch starts clean
300
+ if (daemon._handoffCounts) {
301
+ for (const key of [...daemon._handoffCounts.keys()]) {
302
+ if (key.startsWith(`${defaultTeamId}:`)) daemon._handoffCounts.delete(key);
303
+ }
304
+ }
305
+
306
+ // Spawn phase 1 agents — reuse idle team members with matching roles when possible
307
+ const spawned = [];
308
+ const reused = [];
309
+ const failed = [];
310
+ const phase1Ids = [];
311
+ const teamAgents = daemon.registry.getAll().filter((a) => a.teamId === defaultTeamId);
312
+
313
+ for (const config of phase1) {
314
+ const prompt = config.prompt || '';
315
+
316
+ // Reuse an existing agent with matching role in this team — never spawn
317
+ // duplicates. The team's agents persist across tasks regardless of status.
318
+ const existing = teamAgents.find((a) =>
319
+ a.role === config.role &&
320
+ a.role !== 'planner' &&
321
+ !reused.some((r) => r.id === a.id)
322
+ );
323
+
324
+ if (existing) {
325
+ // Role already exists in this team — never spawn a duplicate.
326
+ // With a prompt: kill+respawn with fresh context and the new task.
327
+ // Without a prompt: keep the existing agent as-is (the planner often
328
+ // emits Mode-1 shaped JSON with empty prompts on follow-up; if we
329
+ // let that fall through to "spawn new", we get 2 backends, 2 fronts).
330
+ if (!prompt) {
331
+ reused.push({ id: existing.id, name: existing.name, role: existing.role, reusedFrom: existing.name });
332
+ phase1Ids.push(existing.id);
333
+ continue;
334
+ }
335
+ try {
336
+ if (existing.status === 'running' || existing.status === 'starting') {
337
+ try { await daemon.processes.kill(existing.id); } catch { /* already dead */ }
338
+ }
339
+ daemon.registry.remove(existing.id);
340
+ daemon.locks.release(existing.id);
341
+
342
+ // Spawn fresh with the same name/team but new prompt + full context
343
+ const validated = validateAgentConfig({
344
+ role: existing.role,
345
+ scope: normalizeScope(config.scope || existing.scope || [], existing.workingDir || projectWorkingDir),
346
+ prompt,
347
+ ...resolveProviderAndModel(config.provider, config.model, existing.provider, existing.model),
348
+ permission: config.permission || existing.permission || 'auto',
349
+ workingDir: existing.workingDir || projectWorkingDir,
350
+ name: existing.name,
351
+ integrationApproval: config.integrationApproval || existing.integrationApproval || undefined,
352
+ reasoningEffort: config.reasoningEffort,
353
+ temperature: config.temperature,
354
+ verbosity: config.verbosity,
355
+ });
356
+ validated.teamId = defaultTeamId;
357
+ const newAgent = await daemon.processes.spawn(validated);
358
+ reused.push({ id: newAgent.id, name: newAgent.name, role: newAgent.role, reusedFrom: existing.name });
359
+ phase1Ids.push(newAgent.id);
360
+ daemon.audit.log('team.reuse', { oldId: existing.id, newId: newAgent.id, role: config.role });
361
+ } catch (err) {
362
+ failed.push({ role: config.role, error: `reuse failed: ${err.message}` });
363
+ }
364
+ } else {
365
+ // No matching agent — spawn a new one
366
+ try {
367
+ const validated = validateAgentConfig({
368
+ role: config.role,
369
+ scope: normalizeScope(config.scope || [], config.workingDir || projectWorkingDir),
370
+ prompt,
371
+ ...resolveProviderAndModel(config.provider, config.model),
372
+ permission: config.permission || 'auto',
373
+ workingDir: config.workingDir || projectWorkingDir,
374
+ name: config.name || undefined,
375
+ integrationApproval: config.integrationApproval || undefined,
376
+ reasoningEffort: config.reasoningEffort,
377
+ temperature: config.temperature,
378
+ verbosity: config.verbosity,
379
+ });
380
+ validated.teamId = defaultTeamId;
381
+ const agent = await daemon.processes.spawn(validated);
382
+ spawned.push({ id: agent.id, name: agent.name, role: agent.role });
383
+ phase1Ids.push(agent.id);
384
+ } catch (err) {
385
+ failed.push({ role: config.role, error: err.message });
386
+ console.log(`[Groove] Failed to spawn ${config.role}: ${err.message}`);
387
+ }
388
+ }
389
+ }
390
+
391
+ if (failed.length > 0) {
392
+ console.warn(`[Groove] Team launch had ${failed.length} failure(s):`, failed.map((f) => `${f.role}: ${f.error}`).join(', '));
393
+ }
394
+
395
+ // Phase 2 agents also scoped to projectWorkingDir
396
+ if (phase2.length > 0 && phase1Ids.length > 0) {
397
+ // Dedup: if a running idle fullstack already exists in this team,
398
+ // skip the phase2 queue — _triggerIdleQC will notify it when phase 1 completes
399
+ const existingQC = teamAgents.find((a) =>
400
+ a.role === 'fullstack' &&
401
+ (a.status === 'running' || a.status === 'starting')
402
+ );
403
+ const qcIsIdle = existingQC && (daemon.journalist?.getAgentFiles(existingQC) || []).length === 0;
404
+
405
+ if (existingQC && qcIsIdle) {
406
+ daemon.audit.log('phase2.skipQueue', { existingQC: existingQC.id, name: existingQC.name, reason: 'idle fullstack exists' });
407
+ } else {
408
+ daemon._pendingPhase2 = daemon._pendingPhase2 || [];
409
+ daemon._pendingPhase2.push({
410
+ waitFor: phase1Ids,
411
+ agents: phase2.map((c) => ({
412
+ role: c.role, scope: c.scope || [], prompt: c.prompt || '',
413
+ ...resolveProviderAndModel(c.provider, c.model),
414
+ permission: c.permission || 'auto',
415
+ reasoningEffort: c.reasoningEffort, temperature: c.temperature, verbosity: c.verbosity,
416
+ workingDir: c.workingDir || projectWorkingDir,
417
+ name: c.name || undefined,
418
+ teamId: defaultTeamId,
419
+ })),
420
+ });
421
+ }
422
+ }
423
+
424
+ // Stash the preview block so the daemon can launch it when the team
425
+ // finishes. The plan file gets deleted seconds after this endpoint returns.
426
+ if (previewBlock && daemon.preview && defaultTeamId) {
427
+ daemon.preview.stashPlan(defaultTeamId, previewBlock, projectWorkingDir, maxPhase, agentConfigs);
428
+ }
429
+
430
+ // Restore the plan if nothing actually spawned or was reused — deleting
431
+ // it on a total failure leaves the team with no recovery path. A failed
432
+ // spawn (scope collision, provider unavailable, etc.) should be retryable
433
+ // once the user fixes the condition.
434
+ if (spawned.length === 0 && reused.length === 0 && failed.length > 0) {
435
+ try { writeFileSync(planPath, planContents); } catch { /* best-effort */ }
436
+ }
437
+
438
+ daemon.audit.log('team.launch', {
439
+ phase1: spawned.length, reused: reused.length, phase2Pending: phase2.length, failed: failed.length,
440
+ agents: [...spawned, ...reused].map((a) => a.role), projectDir: projectDir || null, preview: !!previewBlock,
441
+ });
442
+ res.json({ launched: spawned.length, reused: reused.length, phase2Pending: phase2.length, agents: [...spawned, ...reused], failed, projectDir: projectDir || null, preview: previewBlock ? previewBlock.kind : null });
443
+ } catch (err) {
444
+ res.status(500).json({ error: err.message });
445
+ }
446
+ });
447
+
448
+ // --- Team Templates ---
449
+
450
+ const BUILT_IN_TEMPLATES = {
451
+ 'dev-team': {
452
+ name: 'Dev Team', description: 'Frontend + Backend + QC', icon: 'code',
453
+ roles: [{ role: 'frontend' }, { role: 'backend' }, { role: 'fullstack', phase: 2 }],
454
+ },
455
+ 'full-stack': {
456
+ name: 'Full Stack', description: 'Frontend, Backend, DevOps, Testing + QC', icon: 'layers',
457
+ roles: [{ role: 'frontend' }, { role: 'backend' }, { role: 'devops' }, { role: 'testing' }, { role: 'fullstack', phase: 2 }],
458
+ },
459
+ 'marketing': {
460
+ name: 'Marketing', description: 'CMO, Creative, Analyst', icon: 'megaphone',
461
+ roles: [{ role: 'cmo' }, { role: 'creative' }, { role: 'analyst' }],
462
+ },
463
+ 'business': {
464
+ name: 'Business', description: 'CFO, CMO, Analyst', icon: 'briefcase',
465
+ roles: [{ role: 'cfo' }, { role: 'cmo' }, { role: 'analyst' }],
466
+ },
467
+ 'security-audit': {
468
+ name: 'Security Audit', description: 'Security, Backend + QC', icon: 'shield',
469
+ roles: [{ role: 'security' }, { role: 'backend' }, { role: 'fullstack', phase: 2 }],
470
+ },
471
+ 'docs': {
472
+ name: 'Documentation', description: 'Docs + Frontend', icon: 'file-text',
473
+ roles: [{ role: 'docs' }, { role: 'frontend' }],
474
+ },
475
+ };
476
+
477
+ function getCustomTemplatesDir() {
478
+ return resolve(homedir(), '.groove', 'team-templates');
479
+ }
480
+
481
+ function loadCustomTemplates() {
482
+ const dir = getCustomTemplatesDir();
483
+ if (!existsSync(dir)) return {};
484
+ const templates = {};
485
+ try {
486
+ for (const file of readdirSync(dir).filter(f => f.endsWith('.json'))) {
487
+ try {
488
+ const data = JSON.parse(readFileSync(resolve(dir, file), 'utf8'));
489
+ const key = file.replace(/\.json$/, '');
490
+ templates[key] = { ...data, custom: true };
491
+ } catch { /* skip malformed */ }
492
+ }
493
+ } catch { /* dir read failed */ }
494
+ return templates;
495
+ }
496
+
497
+ app.get('/api/team-templates', (req, res) => {
498
+ const custom = loadCustomTemplates();
499
+ const all = {};
500
+ for (const [k, v] of Object.entries(BUILT_IN_TEMPLATES)) {
501
+ all[k] = { ...v, builtIn: true };
502
+ }
503
+ for (const [k, v] of Object.entries(custom)) {
504
+ all[k] = v;
505
+ }
506
+ res.json(all);
507
+ });
508
+
509
+ app.post('/api/team-templates', (req, res) => {
510
+ const { name, description, icon, roles, settings } = req.body || {};
511
+ if (!name || typeof name !== 'string' || name.length > 64) {
512
+ return res.status(400).json({ error: 'name is required (max 64 chars)' });
513
+ }
514
+ if (!Array.isArray(roles) || roles.length === 0) {
515
+ return res.status(400).json({ error: 'roles array is required' });
516
+ }
517
+ const key = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 64);
518
+ if (!key) return res.status(400).json({ error: 'Invalid template name' });
519
+ if (BUILT_IN_TEMPLATES[key]) {
520
+ return res.status(409).json({ error: 'Cannot overwrite built-in template' });
521
+ }
522
+ const dir = getCustomTemplatesDir();
523
+ mkdirSync(dir, { recursive: true });
524
+ const template = { name, description: description || '', icon: icon || 'users', roles, settings: settings || {} };
525
+ writeFileSync(resolve(dir, `${key}.json`), JSON.stringify(template, null, 2));
526
+ daemon.audit.log('team-template.save', { key, roles: roles.length });
527
+ res.status(201).json({ key, ...template, custom: true });
528
+ });
529
+
530
+ app.delete('/api/team-templates/:name', (req, res) => {
531
+ const key = req.params.name;
532
+ if (BUILT_IN_TEMPLATES[key]) {
533
+ return res.status(403).json({ error: 'Cannot delete built-in template' });
534
+ }
535
+ const file = resolve(getCustomTemplatesDir(), `${key}.json`);
536
+ if (!existsSync(file)) return res.status(404).json({ error: 'Template not found' });
537
+ try {
538
+ unlinkSync(file);
539
+ daemon.audit.log('team-template.delete', { key });
540
+ res.json({ ok: true });
541
+ } catch (err) {
542
+ res.status(500).json({ error: err.message });
543
+ }
544
+ });
545
+
546
+ // --- Team Builder Launch ---
547
+
548
+ app.post('/api/team-builder/launch', async (req, res) => {
549
+ try {
550
+ const { task, roles, settings, launchMode } = req.body || {};
551
+ if (!Array.isArray(roles) || roles.length === 0) {
552
+ return res.status(400).json({ error: 'roles array is required' });
553
+ }
554
+ const mode = launchMode || 'direct';
555
+ const teamSettings = settings || {};
556
+ const teamProvider = teamSettings.provider || daemon.config?.defaultProvider || undefined;
557
+ const teamModel = teamSettings.model || daemon.config?.defaultModel || undefined;
558
+
559
+ const defaultTeamId = req.body.teamId || daemon.teams.getDefault()?.id || null;
560
+ const baseDir = daemon.config?.defaultWorkingDir || daemon.projectDir;
561
+
562
+ if (mode === 'plan-first') {
563
+ const rolesList = roles.map(r => r.role || r.name || r).join(', ');
564
+ const providerNote = teamProvider ? ` (provider: ${teamProvider})` : '';
565
+ let plannerPrompt;
566
+ if (task) {
567
+ plannerPrompt = `The user wants these agents: ${rolesList}${providerNote}. Task: ${task}`;
568
+ } else {
569
+ plannerPrompt = '';
570
+ }
571
+ const plannerConfig = validateAgentConfig({
572
+ role: 'planner',
573
+ prompt: plannerPrompt,
574
+ provider: teamProvider,
575
+ model: teamModel,
576
+ workingDir: baseDir,
577
+ });
578
+ plannerConfig.teamId = defaultTeamId;
579
+ plannerConfig.teamBuilderRoles = roles.map(r => ({ role: r.role || r, provider: r.provider || null }));
580
+ const planner = await daemon.processes.spawn(plannerConfig);
581
+ daemon.audit.log('team-builder.plan-first', { plannerId: planner.id, roles: roles.length });
582
+ return res.status(202).json({ mode: 'plan-first', plannerId: planner.id, message: 'Planner spawned — waiting for user instructions' });
583
+ }
584
+
585
+ const spawned = [];
586
+ const failed = [];
587
+ const phase1Agents = roles.filter(r => !r.phase || r.phase === 1);
588
+ const phase2Agents = roles.filter(r => r.phase === 2);
589
+ const phase1Ids = [];
590
+
591
+ for (const roleDef of phase1Agents) {
592
+ try {
593
+ let prompt = roleDef.prompt || '';
594
+ if (task && mode === 'direct') {
595
+ prompt = task + (prompt ? '\n\n' + prompt : '');
596
+ }
597
+
598
+ const agentConfig = validateAgentConfig({
599
+ role: roleDef.role,
600
+ name: roleDef.name || undefined,
601
+ scope: roleDef.scope || [],
602
+ prompt: mode === 'await' ? '' : prompt,
603
+ provider: roleDef.provider || teamProvider,
604
+ model: roleDef.model || teamModel,
605
+ reasoningEffort: roleDef.reasoningEffort ?? teamSettings.reasoningEffort,
606
+ temperature: roleDef.temperature ?? teamSettings.temperature,
607
+ verbosity: roleDef.verbosity ?? teamSettings.verbosity,
608
+ workingDir: baseDir,
609
+ });
610
+ agentConfig.teamId = defaultTeamId;
611
+ const agent = await daemon.processes.spawn(agentConfig);
612
+ spawned.push({ id: agent.id, name: agent.name, role: agent.role });
613
+ phase1Ids.push(agent.id);
614
+ } catch (err) {
615
+ failed.push({ role: roleDef.role, error: err.message });
616
+ }
617
+ }
618
+
619
+ if (phase2Agents.length > 0 && phase1Ids.length > 0) {
620
+ daemon._pendingPhase2 = daemon._pendingPhase2 || [];
621
+ daemon._pendingPhase2.push({
622
+ waitFor: phase1Ids,
623
+ agents: phase2Agents.map(r => ({
624
+ role: r.role, scope: r.scope || [], prompt: r.prompt || '',
625
+ provider: r.provider || teamProvider || daemon.config?.defaultProvider || undefined,
626
+ model: r.model || teamModel || daemon.config?.defaultModel || 'auto',
627
+ reasoningEffort: r.reasoningEffort ?? teamSettings.reasoningEffort,
628
+ temperature: r.temperature ?? teamSettings.temperature,
629
+ verbosity: r.verbosity ?? teamSettings.verbosity,
630
+ workingDir: baseDir,
631
+ name: r.name || undefined,
632
+ teamId: defaultTeamId,
633
+ })),
634
+ });
635
+ }
636
+
637
+ daemon.audit.log('team-builder.launch', {
638
+ mode, phase1: spawned.length, phase2Pending: phase2Agents.length,
639
+ failed: failed.length, task: task ? task.slice(0, 100) : null,
640
+ });
641
+ res.json({
642
+ mode, launched: spawned.length, phase2Pending: phase2Agents.length,
643
+ agents: spawned, failed,
644
+ });
645
+ } catch (err) {
646
+ res.status(500).json({ error: err.message });
647
+ }
648
+ });
649
+
650
+ }