groove-dev 0.27.94 → 0.27.96

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 (37) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +25 -52
  3. package/node_modules/@groove-dev/cli/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/src/api.js +22 -14
  6. package/node_modules/@groove-dev/daemon/src/introducer.js +21 -6
  7. package/node_modules/@groove-dev/daemon/src/process.js +18 -2
  8. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +27 -28
  9. package/node_modules/@groove-dev/gui/dist/assets/{index-B3GUKInH.js → index-BgTyFy4f.js} +50 -50
  10. package/node_modules/@groove-dev/gui/dist/assets/index-QADLyUj5.css +1 -0
  11. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  12. package/node_modules/@groove-dev/gui/package.json +1 -1
  13. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +68 -16
  14. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +9 -1
  15. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +2 -2
  16. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +1 -1
  17. package/node_modules/@groove-dev/gui/src/stores/groove.js +26 -16
  18. package/node_modules/@groove-dev/gui/src/views/agents.jsx +10 -3
  19. package/package.json +1 -1
  20. package/packages/cli/package.json +1 -1
  21. package/packages/daemon/package.json +1 -1
  22. package/packages/daemon/src/api.js +22 -14
  23. package/packages/daemon/src/introducer.js +21 -6
  24. package/packages/daemon/src/process.js +18 -2
  25. package/packages/daemon/src/providers/gemini.js +27 -28
  26. package/packages/gui/dist/assets/{index-B3GUKInH.js → index-BgTyFy4f.js} +50 -50
  27. package/packages/gui/dist/assets/index-QADLyUj5.css +1 -0
  28. package/packages/gui/dist/index.html +2 -2
  29. package/packages/gui/package.json +1 -1
  30. package/packages/gui/src/components/agents/agent-file-tree.jsx +68 -16
  31. package/packages/gui/src/components/agents/code-review.jsx +9 -1
  32. package/packages/gui/src/components/agents/workspace-mode.jsx +2 -2
  33. package/packages/gui/src/components/editor/code-editor.jsx +1 -1
  34. package/packages/gui/src/stores/groove.js +26 -16
  35. package/packages/gui/src/views/agents.jsx +10 -3
  36. package/node_modules/@groove-dev/gui/dist/assets/index-C1k-GuDg.css +0 -1
  37. package/packages/gui/dist/assets/index-C1k-GuDg.css +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.94",
3
+ "version": "0.27.96",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -3694,18 +3694,26 @@ Keep responses concise. Help them think, don't lecture them about the system the
3694
3694
  const baseDir = daemon.config?.defaultWorkingDir || daemon.projectDir;
3695
3695
 
3696
3696
  if (mode === 'plan-first') {
3697
- // Spawn a headless planner to generate per-agent prompts, then auto-launch
3697
+ const rolesList = roles.map(r => r.role || r.name || r).join(', ');
3698
+ const providerNote = teamProvider ? ` (provider: ${teamProvider})` : '';
3699
+ let plannerPrompt;
3700
+ if (task) {
3701
+ plannerPrompt = `The user wants these agents: ${rolesList}${providerNote}. Task: ${task}`;
3702
+ } else {
3703
+ plannerPrompt = '';
3704
+ }
3698
3705
  const plannerConfig = validateAgentConfig({
3699
3706
  role: 'planner',
3700
- prompt: task || 'Analyze the codebase and create a plan for the team.',
3707
+ prompt: plannerPrompt,
3701
3708
  provider: teamProvider,
3702
3709
  model: teamModel,
3703
3710
  workingDir: baseDir,
3704
3711
  });
3705
3712
  plannerConfig.teamId = defaultTeamId;
3713
+ plannerConfig.teamBuilderRoles = roles.map(r => ({ role: r.role || r, provider: r.provider || null }));
3706
3714
  const planner = await daemon.processes.spawn(plannerConfig);
3707
3715
  daemon.audit.log('team-builder.plan-first', { plannerId: planner.id, roles: roles.length });
3708
- return res.status(202).json({ mode: 'plan-first', plannerId: planner.id, message: 'Planner spawned — team will launch when plan is ready' });
3716
+ return res.status(202).json({ mode: 'plan-first', plannerId: planner.id, message: 'Planner spawned — waiting for user instructions' });
3709
3717
  }
3710
3718
 
3711
3719
  const spawned = [];
@@ -4157,17 +4165,17 @@ Keep responses concise. Help them think, don't lecture them about the system the
4157
4165
  // --- Federation ---
4158
4166
 
4159
4167
  // Federation status (v1 — includes whitelist, connections, ambassadors)
4160
- app.get('/api/federation', proOnly, (req, res) => {
4168
+ app.get('/api/federation', (req, res) => {
4161
4169
  res.json(daemon.federation.getStatus());
4162
4170
  });
4163
4171
 
4164
4172
  // List peers
4165
- app.get('/api/federation/peers', proOnly, (req, res) => {
4173
+ app.get('/api/federation/peers', (req, res) => {
4166
4174
  res.json(daemon.federation.getPeers());
4167
4175
  });
4168
4176
 
4169
4177
  // Unpair a peer
4170
- app.delete('/api/federation/peers/:id', proOnly, (req, res) => {
4178
+ app.delete('/api/federation/peers/:id', (req, res) => {
4171
4179
  try {
4172
4180
  daemon.federation.unpair(req.params.id);
4173
4181
  res.json({ ok: true });
@@ -4177,7 +4185,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4177
4185
  });
4178
4186
 
4179
4187
  // Initiate pairing with a remote daemon
4180
- app.post('/api/federation/initiate', proOnly, async (req, res) => {
4188
+ app.post('/api/federation/initiate', async (req, res) => {
4181
4189
  try {
4182
4190
  const { remoteUrl } = req.body;
4183
4191
  if (!remoteUrl || typeof remoteUrl !== 'string') {
@@ -4192,11 +4200,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
4192
4200
 
4193
4201
  // --- Federation v1: Whitelist ---
4194
4202
 
4195
- app.get('/api/federation/whitelist', proOnly, (req, res) => {
4203
+ app.get('/api/federation/whitelist', (req, res) => {
4196
4204
  res.json(daemon.federation.whitelist?.list() || []);
4197
4205
  });
4198
4206
 
4199
- app.post('/api/federation/whitelist', proOnly, (req, res) => {
4207
+ app.post('/api/federation/whitelist', (req, res) => {
4200
4208
  try {
4201
4209
  const { ip, port, name } = req.body;
4202
4210
  if (!ip || typeof ip !== 'string') {
@@ -4210,7 +4218,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4210
4218
  }
4211
4219
  });
4212
4220
 
4213
- app.delete('/api/federation/whitelist/:ip', proOnly, (req, res) => {
4221
+ app.delete('/api/federation/whitelist/:ip', (req, res) => {
4214
4222
  try {
4215
4223
  daemon.federation.whitelist.remove(req.params.ip);
4216
4224
  daemon.broadcast({ type: 'federation:whitelist', data: daemon.federation.whitelist.list() });
@@ -4248,7 +4256,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4248
4256
 
4249
4257
  // --- Federation v1: Connections ---
4250
4258
 
4251
- app.get('/api/federation/connections', proOnly, (req, res) => {
4259
+ app.get('/api/federation/connections', (req, res) => {
4252
4260
  res.json(daemon.federation.connections?.getStatus() || []);
4253
4261
  });
4254
4262
 
@@ -4271,13 +4279,13 @@ Keep responses concise. Help them think, don't lecture them about the system the
4271
4279
  }
4272
4280
  });
4273
4281
 
4274
- app.get('/api/federation/pouch/log', proOnly, (req, res) => {
4282
+ app.get('/api/federation/pouch/log', (req, res) => {
4275
4283
  const limit = Math.min(parseInt(req.query.limit, 10) || 50, 200);
4276
4284
  res.json(daemon.federation.ambassadors?.getPouchLog(limit) || []);
4277
4285
  });
4278
4286
 
4279
4287
  // Send a pouch message to a peer (local agents/GUI call this)
4280
- app.post('/api/federation/pouch/send', proOnly, async (req, res) => {
4288
+ app.post('/api/federation/pouch/send', async (req, res) => {
4281
4289
  try {
4282
4290
  const { peerId, contract } = req.body;
4283
4291
  if (!peerId || !contract) {
@@ -4323,7 +4331,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4323
4331
  }
4324
4332
  });
4325
4333
 
4326
- app.post('/api/federation/contract/send', proOnly, async (req, res) => {
4334
+ app.post('/api/federation/contract/send', async (req, res) => {
4327
4335
  try {
4328
4336
  const { peerId, contract } = req.body;
4329
4337
  if (!peerId || !contract) {
@@ -48,6 +48,17 @@ export class Introducer {
48
48
  }
49
49
  }
50
50
 
51
+ if (newAgent.teamBuilderRoles && newAgent.teamBuilderRoles.length > 0) {
52
+ lines.push('');
53
+ lines.push('## Team Builder Pre-Selection');
54
+ lines.push('');
55
+ const roleDescs = newAgent.teamBuilderRoles.map(r => {
56
+ return r.provider ? `${r.role} (provider: ${r.provider})` : r.role;
57
+ });
58
+ lines.push(`The user selected these roles in the Team Builder UI: ${roleDescs.join(', ')}.`);
59
+ lines.push('When the user gives you a task, create a plan using EXACTLY these roles. Do not redesign the team composition.');
60
+ }
61
+
51
62
  lines.push('');
52
63
 
53
64
  if (others.length === 0) {
@@ -199,12 +210,16 @@ export class Introducer {
199
210
  // CLAUDE.md parity — non-Claude providers don't read CLAUDE.md natively,
200
211
  // so inject its project content (minus the GROOVE section) into introContext
201
212
  if (newAgent.provider && newAgent.provider !== 'claude-code') {
202
- const claudeMdContent = this._loadClaudeMd(newAgent.workingDir);
203
- if (claudeMdContent) {
204
- lines.push('');
205
- lines.push('## Project Context (from CLAUDE.md)');
206
- lines.push('');
207
- lines.push(claudeMdContent);
213
+ if (newAgent.role === 'planner') {
214
+ // Planners don't need full project context — codebase structure is injected separately
215
+ } else {
216
+ const claudeMdContent = this._loadClaudeMd(newAgent.workingDir);
217
+ if (claudeMdContent) {
218
+ lines.push('');
219
+ lines.push('## Project Context (from CLAUDE.md)');
220
+ lines.push('');
221
+ lines.push(claudeMdContent);
222
+ }
208
223
  }
209
224
  }
210
225
 
@@ -315,7 +315,7 @@ function sanitizeFilename(name) {
315
315
 
316
316
  export function wrapWithRoleReminder(role, message) {
317
317
  if (role === 'planner' && !message.startsWith('ROLE REMINDER:')) {
318
- return 'ROLE REMINDER: You are a PLANNING ONLY agent. Do NOT write code, edit files, or use Edit/Write/Bash tools. Route this task to your team by writing .groove/recommended-team.json.\n\nUser message: ' + message;
318
+ return 'ROLE REMINDER: You are a PLANNING ONLY agent. Do NOT write code, edit source files, or run build commands. Your ONLY file write should be .groove/recommended-team.json.\n\nUser message: ' + message;
319
319
  }
320
320
  return message;
321
321
  }
@@ -1336,7 +1336,23 @@ For normal file edits within your scope, proceed without review.
1336
1336
  if (existsSync(targetPath)) return;
1337
1337
 
1338
1338
  const log = readFileSync(logPath, 'utf8');
1339
- const match = log.match(/\{[\s\S]*?"agents"\s*:\s*\[[\s\S]*?\]\s*\}/);
1339
+
1340
+ // Extract text from JSON log events (Codex agent_message, Claude content blocks)
1341
+ const textParts = [];
1342
+ for (const line of log.split('\n')) {
1343
+ try {
1344
+ const evt = JSON.parse(line.trim());
1345
+ if (evt.item?.type === 'agent_message' && evt.item?.text) textParts.push(evt.item.text);
1346
+ if (evt.type === 'assistant' && evt.message?.content) {
1347
+ for (const block of evt.message.content) {
1348
+ if (block.type === 'text') textParts.push(block.text);
1349
+ }
1350
+ }
1351
+ } catch { textParts.push(line); }
1352
+ }
1353
+ const fullText = textParts.join('\n');
1354
+
1355
+ const match = fullText.match(/\{[\s\S]*?"agents"\s*:\s*\[[\s\S]*?\]\s*\}/);
1340
1356
  if (!match) return;
1341
1357
 
1342
1358
  let parsed;
@@ -147,11 +147,8 @@ export class GeminiProvider extends Provider {
147
147
  }
148
148
 
149
149
  switch (event.type) {
150
- case 'agent_start':
151
- return { type: 'activity', subtype: 'assistant', sessionId: event.streamId, data: [{ type: 'text', text: '' }] };
152
-
153
- case 'session_update':
154
- return null;
150
+ case 'init':
151
+ return { type: 'activity', subtype: 'assistant', sessionId: event.session_id, data: [{ type: 'text', text: '' }] };
155
152
 
156
153
  case 'message': {
157
154
  if (event.role === 'user') return null;
@@ -165,38 +162,39 @@ export class GeminiProvider extends Provider {
165
162
  return { type: 'activity', subtype: 'assistant', data: blocks };
166
163
  }
167
164
 
168
- case 'tool_request': {
169
- const toolName = (event.name || '').includes('shell') || (event.name || '').includes('exec')
170
- ? 'Bash' : event.name || 'Tool';
171
- const input = event.name === 'Bash' || (event.name || '').includes('shell')
172
- ? { command: typeof event.args === 'string' ? event.args : JSON.stringify(event.args || {}) }
173
- : (event.args || {});
165
+ case 'tool_use': {
166
+ const name = event.tool_name || '';
167
+ const toolName = name.includes('shell') || name.includes('exec')
168
+ ? 'Bash' : name || 'Tool';
169
+ const input = toolName === 'Bash'
170
+ ? { command: event.parameters?.command || (typeof event.parameters === 'string' ? event.parameters : JSON.stringify(event.parameters || {})) }
171
+ : (event.parameters || {});
174
172
  return {
175
173
  type: 'activity', subtype: 'assistant',
176
- data: [{ type: 'tool_use', id: event.requestId || 'tool', name: toolName, input }],
174
+ data: [{ type: 'tool_use', id: event.tool_id || 'tool', name: toolName, input }],
177
175
  };
178
176
  }
179
177
 
180
- case 'tool_response': {
181
- const rawContent = event.content;
182
- const contentParts = Array.isArray(rawContent) ? rawContent : (typeof rawContent === 'string' ? [{ text: rawContent }] : rawContent ? [rawContent] : []);
183
- const content = contentParts.map((p) => p.text || '').join('').slice(0, 2000);
184
- const toolName = (event.name || '').includes('shell') || (event.name || '').includes('exec')
185
- ? 'Bash' : event.name || 'Tool';
178
+ case 'tool_result': {
179
+ const content = typeof event.output === 'string' ? event.output.slice(0, 2000) : '';
180
+ const toolId = event.tool_id || '';
181
+ const isShell = toolId.includes('shell') || toolId.includes('exec');
182
+ const toolName = isShell ? 'Bash' : 'Tool';
186
183
  return {
187
184
  type: 'activity', subtype: 'assistant',
188
185
  data: [
189
- { type: 'tool_use', id: event.requestId || 'tool', name: toolName, input: {} },
186
+ { type: 'tool_use', id: toolId || 'tool', name: toolName, input: {} },
190
187
  ...(content ? [{ type: 'text', text: content }] : []),
191
188
  ],
192
189
  };
193
190
  }
194
191
 
195
- case 'usage': {
196
- const inputTokens = event.inputTokens || 0;
197
- const outputTokens = event.outputTokens || 0;
198
- const cachedTokens = event.cachedTokens || 0;
199
- const totalTokens = inputTokens + outputTokens;
192
+ case 'result': {
193
+ const stats = event.stats || {};
194
+ const inputTokens = stats.input_tokens || 0;
195
+ const outputTokens = stats.output_tokens || 0;
196
+ const cachedTokens = stats.cached || 0;
197
+ const totalTokens = stats.total_tokens || (inputTokens + outputTokens);
200
198
 
201
199
  const model = GeminiProvider.models.find((m) => m.id === this._currentModel);
202
200
  const pricing = model?.pricing;
@@ -211,7 +209,8 @@ export class GeminiProvider extends Provider {
211
209
  }
212
210
 
213
211
  return {
214
- type: 'activity', subtype: 'assistant',
212
+ type: 'result',
213
+ subtype: 'assistant',
215
214
  data: [{ type: 'text', text: '' }],
216
215
  tokensUsed: totalTokens,
217
216
  inputTokens,
@@ -220,12 +219,12 @@ export class GeminiProvider extends Provider {
220
219
  contextUsage: inputTokens / maxContext,
221
220
  estimatedCostUsd,
222
221
  costSource: pricing ? 'calculated' : 'estimated',
222
+ cost: estimatedCostUsd,
223
+ duration: stats.duration_ms,
224
+ turns: stats.tool_calls || 0,
223
225
  };
224
226
  }
225
227
 
226
- case 'agent_end':
227
- return { type: 'activity', subtype: 'assistant', data: [{ type: 'text', text: '' }] };
228
-
229
228
  case 'error':
230
229
  return { type: 'activity', subtype: 'assistant', data: [{ type: 'text', text: `Error: ${event.message || 'unknown'}` }] };
231
230