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.
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +1086 -6532
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
- package/node_modules/@groove-dev/daemon/src/index.js +3 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
- package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
- package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
- package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +2 -2
- package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
- package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +889 -0
- package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
- package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
- package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
- package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
- package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
- package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
- package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
- package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
- package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
- package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
- package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
- package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
- package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
- package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
- package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BcoF6_eF.js +1012 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/{packages/gui/src/app.jsx → node_modules/@groove-dev/gui/src/App.jsx} +0 -2
- package/node_modules/@groove-dev/gui/src/app.css +35 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +144 -31
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
- package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
- package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
- package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
- package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
- package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
- package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +195 -14
- package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +286 -102
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
- package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
- package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
- package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
- package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
- package/node_modules/@groove-dev/gui/src/lib/status.js +24 -24
- package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +34 -3144
- package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +452 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +227 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +17 -6
- package/node_modules/@groove-dev/gui/src/views/models.jsx +410 -509
- package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +1086 -6532
- package/packages/daemon/src/gateways/manager.js +35 -1
- package/packages/daemon/src/index.js +3 -0
- package/packages/daemon/src/journalist.js +23 -13
- package/packages/daemon/src/mlx-server.js +365 -0
- package/packages/daemon/src/model-lab.js +308 -12
- package/packages/daemon/src/pm.js +1 -1
- package/packages/daemon/src/process.js +2 -2
- package/packages/daemon/src/providers/local.js +36 -8
- package/packages/daemon/src/registry.js +21 -5
- package/packages/daemon/src/routes/agents.js +889 -0
- package/packages/daemon/src/routes/coordination.js +318 -0
- package/packages/daemon/src/routes/files.js +751 -0
- package/packages/daemon/src/routes/integrations.js +485 -0
- package/packages/daemon/src/routes/network.js +1784 -0
- package/packages/daemon/src/routes/providers.js +755 -0
- package/packages/daemon/src/routes/schedules.js +110 -0
- package/packages/daemon/src/routes/teams.js +650 -0
- package/packages/daemon/src/scheduler.js +456 -24
- package/packages/daemon/src/teams.js +1 -1
- package/packages/daemon/src/validate.js +38 -1
- package/packages/daemon/templates/mlx-setup.json +12 -0
- package/packages/daemon/templates/tgi-setup.json +1 -1
- package/packages/daemon/templates/vllm-setup.json +1 -1
- package/packages/gui/dist/assets/index-BcoF6_eF.js +1012 -0
- package/packages/gui/dist/assets/index-Dd7qhiEd.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/{node_modules/@groove-dev/gui/src/app.jsx → packages/gui/src/App.jsx} +0 -2
- package/packages/gui/src/app.css +35 -0
- package/packages/gui/src/components/agents/agent-config.jsx +1 -128
- package/packages/gui/src/components/agents/agent-feed.jsx +144 -31
- package/packages/gui/src/components/agents/agent-node.jsx +8 -13
- package/packages/gui/src/components/agents/code-review.jsx +159 -122
- package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
- package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
- package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
- package/packages/gui/src/components/automations/automation-card.jsx +274 -0
- package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
- package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
- package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
- package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
- package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
- package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
- package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
- package/packages/gui/src/components/editor/selection-menu.jsx +2 -0
- package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
- package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
- package/packages/gui/src/components/lab/parameter-panel.jsx +195 -14
- package/packages/gui/src/components/lab/runtime-config.jsx +286 -102
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
- package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
- package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
- package/packages/gui/src/components/network/network-health.jsx +2 -2
- package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
- package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
- package/packages/gui/src/components/ui/sheet.jsx +5 -2
- package/packages/gui/src/lib/cron.js +64 -0
- package/packages/gui/src/lib/status.js +24 -24
- package/packages/gui/src/lib/theme-hex.js +1 -0
- package/packages/gui/src/stores/groove.js +34 -3144
- package/packages/gui/src/stores/helpers.js +10 -0
- package/packages/gui/src/stores/slices/agents-slice.js +452 -0
- package/packages/gui/src/stores/slices/automations-slice.js +96 -0
- package/packages/gui/src/stores/slices/chat-slice.js +227 -0
- package/packages/gui/src/stores/slices/editor-slice.js +285 -0
- package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
- package/packages/gui/src/stores/slices/network-slice.js +361 -0
- package/packages/gui/src/stores/slices/preview-slice.js +109 -0
- package/packages/gui/src/stores/slices/providers-slice.js +897 -0
- package/packages/gui/src/stores/slices/teams-slice.js +413 -0
- package/packages/gui/src/stores/slices/ui-slice.js +98 -0
- package/packages/gui/src/views/agents.jsx +5 -5
- package/packages/gui/src/views/dashboard.jsx +12 -13
- package/packages/gui/src/views/marketplace.jsx +191 -3
- package/packages/gui/src/views/model-lab.jsx +17 -6
- package/packages/gui/src/views/models.jsx +410 -509
- package/packages/gui/src/views/network.jsx +3 -3
- package/packages/gui/src/views/settings.jsx +81 -94
- package/packages/gui/src/views/teams.jsx +40 -483
- package/SECURITY_SWEEP.md +0 -228
- package/TRAINING_DATA_v4.md +0 -6
- package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +0 -984
- package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +0 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -322
- package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
- package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
- package/packages/gui/dist/assets/index-Bjd91ufV.js +0 -984
- package/packages/gui/dist/assets/index-BqdwIFn4.css +0 -1
- package/packages/gui/src/components/agents/agent-chat.jsx +0 -322
- package/packages/gui/src/views/preview.jsx +0 -6
- package/packages/gui/src/views/subscription-panel.jsx +0 -327
- 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
|
+
}
|