groove-dev 0.27.88 → 0.27.91

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 (53) hide show
  1. package/CLAUDE.md +0 -11
  2. package/moe-training/client/parsers/claude-code.js +0 -2
  3. package/moe-training/client/session-attestation.js +2 -1
  4. package/moe-training/client/trajectory-capture.js +6 -0
  5. package/moe-training/test/client/parsers/claude-code.test.js +2 -2
  6. package/node_modules/@groove-dev/cli/package.json +1 -1
  7. package/node_modules/@groove-dev/daemon/package.json +1 -1
  8. package/node_modules/@groove-dev/daemon/src/api.js +20 -10
  9. package/node_modules/@groove-dev/daemon/src/conversations.js +32 -6
  10. package/node_modules/@groove-dev/daemon/src/preview.js +1 -1
  11. package/node_modules/@groove-dev/daemon/src/process.js +34 -5
  12. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -1
  13. package/node_modules/@groove-dev/daemon/src/providers/codex.js +1 -1
  14. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +1 -1
  15. package/node_modules/@groove-dev/daemon/src/providers/grok.js +2 -2
  16. package/node_modules/@groove-dev/gui/dist/assets/index-D4vJ_1ET.css +1 -0
  17. package/node_modules/@groove-dev/gui/dist/assets/{index-B_igwWvq.js → index-MLIZRMj1.js} +1734 -1734
  18. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  19. package/node_modules/@groove-dev/gui/package.json +1 -1
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +51 -3
  21. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +24 -10
  22. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +7 -5
  23. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +2 -4
  24. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +14 -14
  25. package/node_modules/@groove-dev/gui/src/stores/groove.js +3 -0
  26. package/node_modules/@groove-dev/gui/src/views/agents.jsx +7 -9
  27. package/package.json +1 -1
  28. package/packages/cli/package.json +1 -1
  29. package/packages/daemon/package.json +1 -1
  30. package/packages/daemon/src/api.js +20 -10
  31. package/packages/daemon/src/conversations.js +32 -6
  32. package/packages/daemon/src/preview.js +1 -1
  33. package/packages/daemon/src/process.js +34 -5
  34. package/packages/daemon/src/providers/claude-code.js +1 -1
  35. package/packages/daemon/src/providers/codex.js +1 -1
  36. package/packages/daemon/src/providers/gemini.js +1 -1
  37. package/packages/daemon/src/providers/grok.js +2 -2
  38. package/packages/gui/dist/assets/index-D4vJ_1ET.css +1 -0
  39. package/packages/gui/dist/assets/{index-B_igwWvq.js → index-MLIZRMj1.js} +1734 -1734
  40. package/packages/gui/dist/index.html +2 -2
  41. package/packages/gui/package.json +1 -1
  42. package/packages/gui/src/components/agents/agent-file-tree.jsx +51 -3
  43. package/packages/gui/src/components/agents/workspace-mode.jsx +24 -10
  44. package/packages/gui/src/components/chat/chat-messages.jsx +7 -5
  45. package/packages/gui/src/components/chat/chat-view.jsx +2 -4
  46. package/packages/gui/src/components/chat/conversation-list.jsx +14 -14
  47. package/packages/gui/src/stores/groove.js +3 -0
  48. package/packages/gui/src/views/agents.jsx +7 -9
  49. package/node_modules/@groove-dev/gui/dist/assets/index-BSqk8cbI.css +0 -1
  50. package/packages/gui/dist/assets/index-BSqk8cbI.css +0 -1
  51. package/test/doomsday-clock/index.html +0 -55
  52. package/test/doomsday-clock/script.js +0 -66
  53. package/test/doomsday-clock/style.css +0 -315
package/CLAUDE.md CHANGED
@@ -263,14 +263,3 @@ Audit-driven release. Multi-agent orchestration system with 7 coordination layer
263
263
  - Dashboard: routing donut, cache panel, context health gauges
264
264
  - Monitor/QC agent mode (stay active, loop)
265
265
  - Distribution: demo video, HN launch, Twitter content
266
-
267
- <!-- GROOVE:START -->
268
- ## GROOVE Orchestration (auto-injected)
269
- Active agents: 2
270
- | Name | Role | Scope |
271
- |------|------|-------|
272
- | fullstack-23 | fullstack | - |
273
- | fullstack-12 | fullstack | - |
274
- See AGENTS_REGISTRY.md for full agent state.
275
- **Memory policy:** GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.
276
- <!-- GROOVE:END -->
@@ -30,7 +30,6 @@ export class ClaudeCodeParser {
30
30
  results.push({
31
31
  type: 'thought',
32
32
  content: block.text || '',
33
- token_count: jsonEvent.message?.usage?.output_tokens || 0,
34
33
  });
35
34
  } else if (block.type === 'tool_use') {
36
35
  this._pendingToolUse.set(block.id, block);
@@ -67,7 +66,6 @@ export class ClaudeCodeParser {
67
66
  return {
68
67
  type: 'resolution',
69
68
  content: typeof jsonEvent.result === 'string' ? jsonEvent.result : JSON.stringify(jsonEvent.result),
70
- token_count: jsonEvent.total_tokens_used || 0,
71
69
  };
72
70
  }
73
71
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { platform, arch, cpus, totalmem, hostname, release, endianness } from 'node:os';
4
4
  import { createHash } from 'node:crypto';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { generateECDHKeypair, deriveSharedSecret, signEnvelope, computeAppHash } from '../shared/crypto.js';
6
7
 
7
8
  export class SessionAttestation {
@@ -14,7 +15,7 @@ export class SessionAttestation {
14
15
  const keypair = generateECDHKeypair();
15
16
  let appVersionHash = '';
16
17
  try {
17
- appVersionHash = computeAppHash(new URL(import.meta.url).pathname);
18
+ appVersionHash = computeAppHash(fileURLToPath(import.meta.url));
18
19
  } catch {
19
20
  appVersionHash = 'unknown';
20
21
  }
@@ -172,6 +172,12 @@ export class TrajectoryCapture {
172
172
  ev.arguments = this._scrubObject(ev.arguments);
173
173
  }
174
174
 
175
+ if (!ev.token_count || ev.token_count < 2) {
176
+ const text = ev.content || '';
177
+ const argsLen = ev.arguments ? JSON.stringify(ev.arguments).length : 0;
178
+ ev.token_count = Math.max(1, Math.ceil((text.length + argsLen) / 4));
179
+ }
180
+
175
181
  const step = {
176
182
  step: ++ctx.stepCount,
177
183
  type: ev.type,
@@ -17,7 +17,7 @@ describe('ClaudeCodeParser', () => {
17
17
  const result = parser.parseEvent(event);
18
18
  assert.equal(result.type, 'thought');
19
19
  assert.equal(result.content, 'I need to fix the bug');
20
- assert.equal(result.token_count, 50);
20
+ assert.equal(result.token_count, undefined);
21
21
  });
22
22
 
23
23
  it('parses tool_use block as action', () => {
@@ -79,7 +79,7 @@ describe('ClaudeCodeParser', () => {
79
79
  const result = parser.parseEvent(event);
80
80
  assert.equal(result.type, 'resolution');
81
81
  assert.equal(result.content, 'Task completed successfully');
82
- assert.equal(result.token_count, 500);
82
+ assert.equal(result.token_count, undefined);
83
83
  });
84
84
 
85
85
  it('extracts tokens from assistant event', () => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.88",
3
+ "version": "0.27.91",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.88",
3
+ "version": "0.27.91",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -146,7 +146,10 @@ export function createApi(app, daemon) {
146
146
  const team = daemon.teams.get(config.teamId);
147
147
  if (team?.workingDir) config.workingDir = team.workingDir;
148
148
  }
149
- // Inherit configured default model if the request didn't pick one
149
+ // Inherit configured defaults if the request didn't pick them
150
+ if (!config.provider && daemon.config?.defaultProvider) {
151
+ config.provider = daemon.config.defaultProvider;
152
+ }
150
153
  if (!config.model && daemon.config?.defaultModel) {
151
154
  config.model = daemon.config.defaultModel;
152
155
  }
@@ -1112,8 +1115,8 @@ export function createApi(app, daemon) {
1112
1115
  app.post('/api/conversations', async (req, res) => {
1113
1116
  try {
1114
1117
  const { provider, model, title, mode, reasoning_effort, verbosity } = req.body;
1115
- if (!provider || typeof provider !== 'string') {
1116
- return res.status(400).json({ error: 'provider is required' });
1118
+ if (provider && typeof provider !== 'string') {
1119
+ return res.status(400).json({ error: 'provider must be a string' });
1117
1120
  }
1118
1121
  if (mode && mode !== 'api' && mode !== 'agent') {
1119
1122
  return res.status(400).json({ error: 'mode must be "api" or "agent"' });
@@ -4501,8 +4504,10 @@ Keep responses concise. Help them think, don't lecture them about the system the
4501
4504
  }
4502
4505
 
4503
4506
  daemon.config.defaultProvider = provider;
4507
+ daemon.config.defaultChatProvider = provider;
4504
4508
  if (model && typeof model === 'string' && model.length <= 100) {
4505
4509
  daemon.config.defaultModel = model.trim();
4510
+ daemon.config.defaultChatModel = model.trim();
4506
4511
  }
4507
4512
  const { saveConfig } = await import('./firstrun.js');
4508
4513
  saveConfig(daemon.grooveDir, daemon.config);
@@ -4534,15 +4539,20 @@ Keep responses concise. Help them think, don't lecture them about the system the
4534
4539
  saveConfig(daemon.grooveDir, daemon.config);
4535
4540
 
4536
4541
  if (enabled) {
4537
- const userId = ConsentManager.getOrCreateUserId();
4538
- const consent = new ConsentManager();
4539
4542
  try {
4540
- consent.recordConsent(userId, true, '1.0');
4541
- } finally {
4542
- consent.close();
4543
+ const userId = ConsentManager.getOrCreateUserId();
4544
+ const consent = new ConsentManager();
4545
+ try {
4546
+ consent.recordConsent(userId, true, '1.0');
4547
+ } finally {
4548
+ consent.close();
4549
+ }
4550
+ await daemon._initTrajectoryCapture();
4551
+ daemon.state.set('training_enrolled_at', new Date().toISOString());
4552
+ } catch (e) {
4553
+ daemon.config.training_opt_in = false;
4554
+ return res.status(500).json({ error: 'Failed to enable data sharing', detail: e.message });
4543
4555
  }
4544
- await daemon._initTrajectoryCapture();
4545
- daemon.state.set('training_enrolled_at', new Date().toISOString());
4546
4556
  } else {
4547
4557
  if (daemon.trajectoryCapture) {
4548
4558
  try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
@@ -55,6 +55,22 @@ export class ConversationManager {
55
55
  return null;
56
56
  }
57
57
 
58
+ _resolveAutoProviderModel(preferredProvider) {
59
+ const priority = ['claude-code', 'codex', 'gemini', 'grok', 'ollama'];
60
+ const candidates = preferredProvider ? [preferredProvider] : priority;
61
+
62
+ for (const pid of candidates) {
63
+ if (!isProviderInstalled(pid)) continue;
64
+ const p = getProvider(pid);
65
+ if (!p) continue;
66
+ const models = p.constructor.models || [];
67
+ const chatModel = models.find((m) => m.type !== 'image') || models[0];
68
+ if (chatModel) return { provider: pid, model: chatModel.id };
69
+ }
70
+
71
+ return { provider: 'claude-code', model: 'claude-sonnet-4-6' };
72
+ }
73
+
58
74
  async create(provider, model, title, mode = 'api', options = {}) {
59
75
  if (!provider && this.daemon.config?.defaultChatProvider) {
60
76
  provider = this.daemon.config.defaultChatProvider;
@@ -63,6 +79,12 @@ export class ConversationManager {
63
79
  model = this.daemon.config.defaultChatModel;
64
80
  }
65
81
 
82
+ if (!provider || !model) {
83
+ const resolved = this._resolveAutoProviderModel(provider);
84
+ if (!provider) provider = resolved.provider;
85
+ if (!model) model = resolved.model;
86
+ }
87
+
66
88
  const id = randomUUID().slice(0, 12);
67
89
  const now = new Date().toISOString();
68
90
 
@@ -328,12 +350,16 @@ export class ConversationManager {
328
350
  let providerName = conv.provider;
329
351
 
330
352
  if (!provider || !isProviderInstalled(conv.provider)) {
331
- const priority = ['claude-code', 'gemini', 'codex', 'ollama'];
332
- const fallbackId = priority.find((p) => isProviderInstalled(p));
333
- if (!fallbackId) throw new Error('No provider available for chat');
334
- provider = getProvider(fallbackId);
335
- providerName = fallbackId;
336
- modelId = null;
353
+ const resolved = this._resolveAutoProviderModel(null);
354
+ provider = getProvider(resolved.provider);
355
+ if (!provider) throw new Error('No provider available for chat');
356
+ providerName = resolved.provider;
357
+ modelId = resolved.model;
358
+ }
359
+
360
+ if (!modelId) {
361
+ const resolved = this._resolveAutoProviderModel(providerName);
362
+ modelId = resolved.model;
337
363
  }
338
364
 
339
365
  // Build messages array for direct API call
@@ -191,7 +191,7 @@ export class PreviewService {
191
191
  // expansion, and shell builtins work as the planner wrote them.
192
192
  const proc = cpSpawn('bash', ['-lc', command], {
193
193
  cwd: baseDir,
194
- env: { ...process.env, FORCE_COLOR: '0', CI: '' },
194
+ env: { ...process.env, FORCE_COLOR: '0', CI: '', BROWSER: 'none' },
195
195
  stdio: ['ignore', 'pipe', 'pipe'],
196
196
  detached: false,
197
197
  });
@@ -432,10 +432,17 @@ export class ProcessManager {
432
432
  if (installed.length === 0) {
433
433
  throw new Error('No AI providers installed. Install Claude Code, Gemini CLI, Codex, or Ollama first.');
434
434
  }
435
- // Priority: claude-code > gemini > codex > local (local replaces ollama in UI)
436
- const priority = ['claude-code', 'gemini', 'codex', 'local', 'ollama'];
437
- const best = priority.find((p) => installed.some((i) => i.id === p)) || installed[0].id;
438
- providerName = best;
435
+ // If a model is specified, find the provider that supports it
436
+ if (config.model && config.model !== 'auto') {
437
+ const match = installed.find((p) => p.models?.some((m) => m.id === config.model));
438
+ if (match) providerName = match.id;
439
+ }
440
+ // Fallback: priority-based selection
441
+ if (!providerName) {
442
+ const priority = ['claude-code', 'gemini', 'codex', 'local', 'ollama'];
443
+ const best = priority.find((p) => installed.some((i) => i.id === p)) || installed[0].id;
444
+ providerName = best;
445
+ }
439
446
  }
440
447
 
441
448
  const provider = getProvider(providerName);
@@ -1282,6 +1289,10 @@ For normal file edits within your scope, proceed without review.
1282
1289
  }
1283
1290
  }
1284
1291
 
1292
+ // Phase 2 spawns are async — _checkPhase2 may have consumed the pending
1293
+ // entry but the spawn() calls haven't resolved yet. Wait for them.
1294
+ if (this._phase2Spawning?.has(teamId)) return;
1295
+
1285
1296
  const teamAgents = this.daemon.registry.getAll().filter((a) => a.teamId === teamId && a.role !== 'planner');
1286
1297
  if (teamAgents.length === 0) return;
1287
1298
  const terminal = new Set(['completed', 'crashed', 'stopped', 'killed']);
@@ -1367,6 +1378,12 @@ For normal file edits within your scope, proceed without review.
1367
1378
  return files.length === 0;
1368
1379
  });
1369
1380
 
1381
+ // Track that phase 2 spawns are in-flight for this team so
1382
+ // _checkPreviewReady doesn't race ahead of the async spawn calls.
1383
+ const groupTeamId = group.agents[0]?.teamId || this.daemon.teams.getDefault()?.id || null;
1384
+ if (!this._phase2Spawning) this._phase2Spawning = new Map();
1385
+ const spawnPromises = [];
1386
+
1370
1387
  // Auto-spawn phase 2 agents — if phase 1 was idle, clear the prompt
1371
1388
  // so QC also waits for instructions instead of auditing nothing
1372
1389
  for (const config of group.agents) {
@@ -1406,7 +1423,7 @@ For normal file edits within your scope, proceed without review.
1406
1423
  try {
1407
1424
  const validated = validateAgentConfig(config);
1408
1425
  if (!validated.teamId) validated.teamId = this.daemon.teams.getDefault()?.id || null;
1409
- this.spawn(validated).then((agent) => {
1426
+ const p = this.spawn(validated).then((agent) => {
1410
1427
  this.daemon.broadcast({
1411
1428
  type: 'phase2:spawned',
1412
1429
  agentId: agent.id,
@@ -1422,6 +1439,7 @@ For normal file edits within your scope, proceed without review.
1422
1439
  error: err.message,
1423
1440
  });
1424
1441
  });
1442
+ spawnPromises.push(p);
1425
1443
  } catch (err) {
1426
1444
  console.error(`[Groove] Phase 2 config invalid for ${config.role}: ${err.message}`);
1427
1445
  this.daemon.broadcast({
@@ -1431,6 +1449,17 @@ For normal file edits within your scope, proceed without review.
1431
1449
  });
1432
1450
  }
1433
1451
  }
1452
+
1453
+ // Mark this team as having phase 2 spawns in-flight. Cleared once
1454
+ // all spawn promises settle so _checkPreviewReady won't race ahead.
1455
+ if (spawnPromises.length > 0 && groupTeamId) {
1456
+ this._phase2Spawning.set(groupTeamId, (this._phase2Spawning.get(groupTeamId) || 0) + spawnPromises.length);
1457
+ Promise.allSettled(spawnPromises).then(() => {
1458
+ const remaining = (this._phase2Spawning.get(groupTeamId) || 0) - spawnPromises.length;
1459
+ if (remaining <= 0) this._phase2Spawning.delete(groupTeamId);
1460
+ else this._phase2Spawning.set(groupTeamId, remaining);
1461
+ });
1462
+ }
1434
1463
  }
1435
1464
  }
1436
1465
  }
@@ -282,7 +282,7 @@ export class ClaudeCodeProvider extends Provider {
282
282
  let finished = false;
283
283
  const finish = () => { if (!finished) { finished = true; onDone(); } };
284
284
  const body = JSON.stringify({
285
- model: model || 'claude-sonnet-4-6',
285
+ model,
286
286
  messages,
287
287
  max_tokens: 8192,
288
288
  stream: true,
@@ -163,7 +163,7 @@ export class CodexProvider extends Provider {
163
163
  const effort = reasoningEffort || 'medium';
164
164
  const verb = verbosity || 'medium';
165
165
  const body = {
166
- model: model || 'gpt-5.5',
166
+ model,
167
167
  input: previousResponseId ? [messages[messages.length - 1]] : messages,
168
168
  stream: true,
169
169
  reasoning: { effort },
@@ -94,7 +94,7 @@ export class GeminiProvider extends Provider {
94
94
  const controller = new AbortController();
95
95
  let finished = false;
96
96
  const finish = () => { if (!finished) { finished = true; onDone(); } };
97
- const m = model || 'gemini-2.5-flash';
97
+ const m = model;
98
98
  const contents = messages.map((msg) => ({
99
99
  role: msg.role === 'assistant' ? 'model' : 'user',
100
100
  parts: [{ text: msg.content }],
@@ -58,7 +58,7 @@ export class GrokProvider extends Provider {
58
58
  getLoopConfig(agent) {
59
59
  return {
60
60
  apiBase: 'https://api.x.ai/v1',
61
- model: agent.model || 'grok-4-1-fast',
61
+ model: agent.model,
62
62
  contextWindow: 131072,
63
63
  temperature: 0.1,
64
64
  maxResponseTokens: 16384,
@@ -97,7 +97,7 @@ export class GrokProvider extends Provider {
97
97
  'Content-Type': 'application/json',
98
98
  },
99
99
  body: JSON.stringify({
100
- model: model || 'grok-4-1-fast',
100
+ model,
101
101
  messages,
102
102
  stream: true,
103
103
  }),