groove-dev 0.27.91 → 0.27.92

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 (68) 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 +228 -3
  4. package/node_modules/@groove-dev/daemon/src/introducer.js +42 -0
  5. package/node_modules/@groove-dev/daemon/src/process.js +5 -1
  6. package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
  7. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +8 -0
  8. package/node_modules/@groove-dev/daemon/src/providers/codex.js +33 -4
  9. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +14 -1
  10. package/node_modules/@groove-dev/daemon/src/providers/grok.js +8 -1
  11. package/node_modules/@groove-dev/daemon/src/providers/local.js +8 -1
  12. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +74 -5
  13. package/node_modules/@groove-dev/daemon/src/validate.js +22 -1
  14. package/node_modules/@groove-dev/gui/dist/assets/index-Bo6AeNmM.css +1 -0
  15. package/node_modules/@groove-dev/gui/dist/assets/index-DWv32qyJ.js +8653 -0
  16. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  17. package/node_modules/@groove-dev/gui/package.json +1 -1
  18. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -44
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +29 -28
  20. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +53 -143
  21. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +3 -30
  22. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +163 -153
  23. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +15 -5
  24. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +26 -17
  25. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +29 -23
  26. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +5 -1
  27. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +9 -5
  28. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +5 -1
  29. package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +50 -0
  30. package/node_modules/@groove-dev/gui/src/stores/groove.js +145 -9
  31. package/node_modules/@groove-dev/gui/src/views/agents.jsx +707 -14
  32. package/package.json +1 -1
  33. package/packages/cli/package.json +1 -1
  34. package/packages/daemon/package.json +1 -1
  35. package/packages/daemon/src/api.js +228 -3
  36. package/packages/daemon/src/introducer.js +42 -0
  37. package/packages/daemon/src/process.js +5 -1
  38. package/packages/daemon/src/providers/base.js +4 -0
  39. package/packages/daemon/src/providers/claude-code.js +8 -0
  40. package/packages/daemon/src/providers/codex.js +33 -4
  41. package/packages/daemon/src/providers/gemini.js +14 -1
  42. package/packages/daemon/src/providers/grok.js +8 -1
  43. package/packages/daemon/src/providers/local.js +8 -1
  44. package/packages/daemon/src/tunnel-manager.js +74 -5
  45. package/packages/daemon/src/validate.js +22 -1
  46. package/packages/gui/dist/assets/index-Bo6AeNmM.css +1 -0
  47. package/packages/gui/dist/assets/index-DWv32qyJ.js +8653 -0
  48. package/packages/gui/dist/index.html +2 -2
  49. package/packages/gui/package.json +1 -1
  50. package/packages/gui/src/components/agents/agent-chat.jsx +26 -44
  51. package/packages/gui/src/components/agents/agent-file-tree.jsx +29 -28
  52. package/packages/gui/src/components/agents/workspace-mode.jsx +53 -143
  53. package/packages/gui/src/components/chat/chat-header.jsx +3 -30
  54. package/packages/gui/src/components/chat/chat-input.jsx +163 -153
  55. package/packages/gui/src/components/chat/chat-view.jsx +15 -5
  56. package/packages/gui/src/components/chat/conversation-list.jsx +26 -17
  57. package/packages/gui/src/components/editor/code-editor.jsx +29 -23
  58. package/packages/gui/src/components/settings/quick-connect.jsx +5 -1
  59. package/packages/gui/src/components/settings/remote-server-card.jsx +9 -5
  60. package/packages/gui/src/components/settings/ssh-wizard.jsx +5 -1
  61. package/packages/gui/src/components/ui/slider.jsx +50 -0
  62. package/packages/gui/src/stores/groove.js +145 -9
  63. package/packages/gui/src/views/agents.jsx +707 -14
  64. package/workspace.png +0 -0
  65. package/node_modules/@groove-dev/gui/dist/assets/index-D4vJ_1ET.css +0 -1
  66. package/node_modules/@groove-dev/gui/dist/assets/index-MLIZRMj1.js +0 -8642
  67. package/packages/gui/dist/assets/index-D4vJ_1ET.css +0 -1
  68. package/packages/gui/dist/assets/index-MLIZRMj1.js +0 -8642
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.91",
3
+ "version": "0.27.92",
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.91",
3
+ "version": "0.27.92",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -3332,7 +3332,14 @@ Keep responses concise. Help them think, don't lecture them about the system the
3332
3332
  let agentConfigs;
3333
3333
  let projectDir = null;
3334
3334
  let previewBlock = null;
3335
- if (Array.isArray(raw)) {
3335
+
3336
+ // Frontend Team Builder override — if body.agents is provided, use it
3337
+ // instead of the planner's recommended-team.json
3338
+ if (Array.isArray(req.body?.agents) && req.body.agents.length > 0) {
3339
+ agentConfigs = req.body.agents;
3340
+ projectDir = raw.projectDir || null;
3341
+ previewBlock = raw.preview || null;
3342
+ } else if (Array.isArray(raw)) {
3336
3343
  agentConfigs = raw;
3337
3344
  } else if (raw && Array.isArray(raw.agents)) {
3338
3345
  agentConfigs = raw.agents;
@@ -3383,6 +3390,23 @@ Keep responses concise. Help them think, don't lecture them about the system the
3383
3390
  });
3384
3391
  }
3385
3392
 
3393
+ // Team-level overrides from the pre-planner config panel
3394
+ const teamProvider = req.body?.teamProvider || undefined;
3395
+ const teamModel = req.body?.teamModel || undefined;
3396
+ const teamReasoningEffort = req.body?.teamReasoningEffort != null ? Number(req.body.teamReasoningEffort) : undefined;
3397
+ const teamTemperature = req.body?.teamTemperature != null ? Number(req.body.teamTemperature) : undefined;
3398
+ const teamVerbosity = req.body?.teamVerbosity != null ? Number(req.body.teamVerbosity) : undefined;
3399
+
3400
+ if (teamProvider || teamModel) {
3401
+ for (const c of agentConfigs) {
3402
+ if (teamProvider) c.provider = teamProvider;
3403
+ if (teamModel) c.model = teamModel;
3404
+ if (teamReasoningEffort !== undefined) c.reasoningEffort = teamReasoningEffort;
3405
+ if (teamTemperature !== undefined) c.temperature = teamTemperature;
3406
+ if (teamVerbosity !== undefined) c.verbosity = teamVerbosity;
3407
+ }
3408
+ }
3409
+
3386
3410
  // Separate phase 1 (builders) and phase 2 (QC/finisher)
3387
3411
  const phase1 = agentConfigs.filter((a) => !a.phase || a.phase === 1);
3388
3412
  let phase2 = agentConfigs.filter((a) => a.phase === 2);
@@ -3450,6 +3474,9 @@ Keep responses concise. Help them think, don't lecture them about the system the
3450
3474
  workingDir: existing.workingDir || projectWorkingDir,
3451
3475
  name: existing.name,
3452
3476
  integrationApproval: config.integrationApproval || existing.integrationApproval || undefined,
3477
+ reasoningEffort: config.reasoningEffort,
3478
+ temperature: config.temperature,
3479
+ verbosity: config.verbosity,
3453
3480
  });
3454
3481
  validated.teamId = defaultTeamId;
3455
3482
  const newAgent = await daemon.processes.spawn(validated);
@@ -3466,12 +3493,15 @@ Keep responses concise. Help them think, don't lecture them about the system the
3466
3493
  role: config.role,
3467
3494
  scope: normalizeScope(config.scope || [], config.workingDir || projectWorkingDir),
3468
3495
  prompt,
3469
- provider: config.provider || undefined,
3496
+ provider: config.provider || daemon.config?.defaultProvider || undefined,
3470
3497
  model: config.model || daemon.config?.defaultModel || 'auto',
3471
3498
  permission: config.permission || 'auto',
3472
3499
  workingDir: config.workingDir || projectWorkingDir,
3473
3500
  name: config.name || undefined,
3474
3501
  integrationApproval: config.integrationApproval || undefined,
3502
+ reasoningEffort: config.reasoningEffort,
3503
+ temperature: config.temperature,
3504
+ verbosity: config.verbosity,
3475
3505
  });
3476
3506
  validated.teamId = defaultTeamId;
3477
3507
  const agent = await daemon.processes.spawn(validated);
@@ -3506,8 +3536,9 @@ Keep responses concise. Help them think, don't lecture them about the system the
3506
3536
  waitFor: phase1Ids,
3507
3537
  agents: phase2.map((c) => ({
3508
3538
  role: c.role, scope: c.scope || [], prompt: c.prompt || '',
3509
- provider: c.provider || undefined, model: c.model || 'auto',
3539
+ provider: c.provider || daemon.config?.defaultProvider || undefined, model: c.model || daemon.config?.defaultModel || 'auto',
3510
3540
  permission: c.permission || 'auto',
3541
+ reasoningEffort: c.reasoningEffort, temperature: c.temperature, verbosity: c.verbosity,
3511
3542
  workingDir: c.workingDir || projectWorkingDir,
3512
3543
  name: c.name || undefined,
3513
3544
  teamId: defaultTeamId,
@@ -3540,6 +3571,200 @@ Keep responses concise. Help them think, don't lecture them about the system the
3540
3571
  }
3541
3572
  });
3542
3573
 
3574
+ // --- Team Templates ---
3575
+
3576
+ const BUILT_IN_TEMPLATES = {
3577
+ 'dev-team': {
3578
+ name: 'Dev Team', description: 'Frontend + Backend + QC', icon: 'code',
3579
+ roles: [{ role: 'frontend' }, { role: 'backend' }, { role: 'fullstack', phase: 2 }],
3580
+ },
3581
+ 'full-stack': {
3582
+ name: 'Full Stack', description: 'Frontend, Backend, DevOps, Testing + QC', icon: 'layers',
3583
+ roles: [{ role: 'frontend' }, { role: 'backend' }, { role: 'devops' }, { role: 'testing' }, { role: 'fullstack', phase: 2 }],
3584
+ },
3585
+ 'marketing': {
3586
+ name: 'Marketing', description: 'CMO, Creative, Analyst', icon: 'megaphone',
3587
+ roles: [{ role: 'cmo' }, { role: 'creative' }, { role: 'analyst' }],
3588
+ },
3589
+ 'business': {
3590
+ name: 'Business', description: 'CFO, CMO, Analyst', icon: 'briefcase',
3591
+ roles: [{ role: 'cfo' }, { role: 'cmo' }, { role: 'analyst' }],
3592
+ },
3593
+ 'security-audit': {
3594
+ name: 'Security Audit', description: 'Security, Backend + QC', icon: 'shield',
3595
+ roles: [{ role: 'security' }, { role: 'backend' }, { role: 'fullstack', phase: 2 }],
3596
+ },
3597
+ 'docs': {
3598
+ name: 'Documentation', description: 'Docs + Frontend', icon: 'file-text',
3599
+ roles: [{ role: 'docs' }, { role: 'frontend' }],
3600
+ },
3601
+ };
3602
+
3603
+ function getCustomTemplatesDir() {
3604
+ return resolve(homedir(), '.groove', 'team-templates');
3605
+ }
3606
+
3607
+ function loadCustomTemplates() {
3608
+ const dir = getCustomTemplatesDir();
3609
+ if (!existsSync(dir)) return {};
3610
+ const templates = {};
3611
+ try {
3612
+ for (const file of readdirSync(dir).filter(f => f.endsWith('.json'))) {
3613
+ try {
3614
+ const data = JSON.parse(readFileSync(resolve(dir, file), 'utf8'));
3615
+ const key = file.replace(/\.json$/, '');
3616
+ templates[key] = { ...data, custom: true };
3617
+ } catch { /* skip malformed */ }
3618
+ }
3619
+ } catch { /* dir read failed */ }
3620
+ return templates;
3621
+ }
3622
+
3623
+ app.get('/api/team-templates', (req, res) => {
3624
+ const custom = loadCustomTemplates();
3625
+ const all = {};
3626
+ for (const [k, v] of Object.entries(BUILT_IN_TEMPLATES)) {
3627
+ all[k] = { ...v, builtIn: true };
3628
+ }
3629
+ for (const [k, v] of Object.entries(custom)) {
3630
+ all[k] = v;
3631
+ }
3632
+ res.json(all);
3633
+ });
3634
+
3635
+ app.post('/api/team-templates', (req, res) => {
3636
+ const { name, description, icon, roles, settings } = req.body || {};
3637
+ if (!name || typeof name !== 'string' || name.length > 64) {
3638
+ return res.status(400).json({ error: 'name is required (max 64 chars)' });
3639
+ }
3640
+ if (!Array.isArray(roles) || roles.length === 0) {
3641
+ return res.status(400).json({ error: 'roles array is required' });
3642
+ }
3643
+ const key = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 64);
3644
+ if (!key) return res.status(400).json({ error: 'Invalid template name' });
3645
+ if (BUILT_IN_TEMPLATES[key]) {
3646
+ return res.status(409).json({ error: 'Cannot overwrite built-in template' });
3647
+ }
3648
+ const dir = getCustomTemplatesDir();
3649
+ mkdirSync(dir, { recursive: true });
3650
+ const template = { name, description: description || '', icon: icon || 'users', roles, settings: settings || {} };
3651
+ writeFileSync(resolve(dir, `${key}.json`), JSON.stringify(template, null, 2));
3652
+ daemon.audit.log('team-template.save', { key, roles: roles.length });
3653
+ res.status(201).json({ key, ...template, custom: true });
3654
+ });
3655
+
3656
+ app.delete('/api/team-templates/:name', (req, res) => {
3657
+ const key = req.params.name;
3658
+ if (BUILT_IN_TEMPLATES[key]) {
3659
+ return res.status(403).json({ error: 'Cannot delete built-in template' });
3660
+ }
3661
+ const file = resolve(getCustomTemplatesDir(), `${key}.json`);
3662
+ if (!existsSync(file)) return res.status(404).json({ error: 'Template not found' });
3663
+ try {
3664
+ unlinkSync(file);
3665
+ daemon.audit.log('team-template.delete', { key });
3666
+ res.json({ ok: true });
3667
+ } catch (err) {
3668
+ res.status(500).json({ error: err.message });
3669
+ }
3670
+ });
3671
+
3672
+ // --- Team Builder Launch ---
3673
+
3674
+ app.post('/api/team-builder/launch', async (req, res) => {
3675
+ try {
3676
+ const { task, roles, settings, launchMode } = req.body || {};
3677
+ if (!Array.isArray(roles) || roles.length === 0) {
3678
+ return res.status(400).json({ error: 'roles array is required' });
3679
+ }
3680
+ const mode = launchMode || 'direct';
3681
+ const teamSettings = settings || {};
3682
+ const teamProvider = teamSettings.provider || daemon.config?.defaultProvider || undefined;
3683
+ const teamModel = teamSettings.model || daemon.config?.defaultModel || undefined;
3684
+
3685
+ const defaultTeamId = req.body.teamId || daemon.teams.getDefault()?.id || null;
3686
+ const baseDir = daemon.config?.defaultWorkingDir || daemon.projectDir;
3687
+
3688
+ if (mode === 'plan-first') {
3689
+ // Spawn a headless planner to generate per-agent prompts, then auto-launch
3690
+ const plannerConfig = validateAgentConfig({
3691
+ role: 'planner',
3692
+ prompt: task || 'Analyze the codebase and create a plan for the team.',
3693
+ provider: teamProvider,
3694
+ model: teamModel,
3695
+ workingDir: baseDir,
3696
+ });
3697
+ plannerConfig.teamId = defaultTeamId;
3698
+ const planner = await daemon.processes.spawn(plannerConfig);
3699
+ daemon.audit.log('team-builder.plan-first', { plannerId: planner.id, roles: roles.length });
3700
+ return res.status(202).json({ mode: 'plan-first', plannerId: planner.id, message: 'Planner spawned — team will launch when plan is ready' });
3701
+ }
3702
+
3703
+ const spawned = [];
3704
+ const failed = [];
3705
+ const phase1Agents = roles.filter(r => !r.phase || r.phase === 1);
3706
+ const phase2Agents = roles.filter(r => r.phase === 2);
3707
+ const phase1Ids = [];
3708
+
3709
+ for (const roleDef of phase1Agents) {
3710
+ try {
3711
+ let prompt = roleDef.prompt || '';
3712
+ if (task && mode === 'direct') {
3713
+ prompt = task + (prompt ? '\n\n' + prompt : '');
3714
+ }
3715
+
3716
+ const agentConfig = validateAgentConfig({
3717
+ role: roleDef.role,
3718
+ name: roleDef.name || undefined,
3719
+ scope: roleDef.scope || [],
3720
+ prompt: mode === 'await' ? '' : prompt,
3721
+ provider: roleDef.provider || teamProvider,
3722
+ model: roleDef.model || teamModel,
3723
+ reasoningEffort: roleDef.reasoningEffort ?? teamSettings.reasoningEffort,
3724
+ temperature: roleDef.temperature ?? teamSettings.temperature,
3725
+ verbosity: roleDef.verbosity ?? teamSettings.verbosity,
3726
+ workingDir: baseDir,
3727
+ });
3728
+ agentConfig.teamId = defaultTeamId;
3729
+ const agent = await daemon.processes.spawn(agentConfig);
3730
+ spawned.push({ id: agent.id, name: agent.name, role: agent.role });
3731
+ phase1Ids.push(agent.id);
3732
+ } catch (err) {
3733
+ failed.push({ role: roleDef.role, error: err.message });
3734
+ }
3735
+ }
3736
+
3737
+ if (phase2Agents.length > 0 && phase1Ids.length > 0) {
3738
+ daemon._pendingPhase2 = daemon._pendingPhase2 || [];
3739
+ daemon._pendingPhase2.push({
3740
+ waitFor: phase1Ids,
3741
+ agents: phase2Agents.map(r => ({
3742
+ role: r.role, scope: r.scope || [], prompt: r.prompt || '',
3743
+ provider: r.provider || teamProvider || daemon.config?.defaultProvider || undefined,
3744
+ model: r.model || teamModel || daemon.config?.defaultModel || 'auto',
3745
+ reasoningEffort: r.reasoningEffort ?? teamSettings.reasoningEffort,
3746
+ temperature: r.temperature ?? teamSettings.temperature,
3747
+ verbosity: r.verbosity ?? teamSettings.verbosity,
3748
+ workingDir: baseDir,
3749
+ name: r.name || undefined,
3750
+ teamId: defaultTeamId,
3751
+ })),
3752
+ });
3753
+ }
3754
+
3755
+ daemon.audit.log('team-builder.launch', {
3756
+ mode, phase1: spawned.length, phase2Pending: phase2Agents.length,
3757
+ failed: failed.length, task: task ? task.slice(0, 100) : null,
3758
+ });
3759
+ res.json({
3760
+ mode, launched: spawned.length, phase2Pending: phase2Agents.length,
3761
+ agents: spawned, failed,
3762
+ });
3763
+ } catch (err) {
3764
+ res.status(500).json({ error: err.message });
3765
+ }
3766
+ });
3767
+
3543
3768
  // Preview service — one-click View Site for completed teams
3544
3769
  app.get('/api/preview', (req, res) => {
3545
3770
  res.json({ previews: daemon.preview?.list() || [] });
@@ -103,6 +103,7 @@ export class Introducer {
103
103
  lines.push(`- NEVER kill the daemon process ("kill <pid>", "pkill groove", "killall node")`);
104
104
  lines.push(`- NEVER run "./promote.sh", "./promote-local.sh", or any publish/deploy script`);
105
105
  lines.push(`- NEVER start long-running dev servers that block process exit (vite dev, npm start, next dev)`);
106
+ lines.push(`- NEVER open files in a browser. No "open index.html", "open http://...", "xdg-open", or any command that launches a browser window. GROOVE has its own preview system — the user will view the site there.`);
106
107
  lines.push(`If code changes require a daemon restart to take effect, state that in your output so the user can restart manually. Do NOT restart it yourself.`);
107
108
 
108
109
  // User feedback from previous tasks — critical context about what the user
@@ -195,6 +196,18 @@ export class Introducer {
195
196
  lines.push(`GROOVE_PROJECT_MAP.md contains a structural overview of this project. This is BACKGROUND INFORMATION ONLY — it is NOT your task. Do not treat existing files or previous work as something you should continue or improve unless the user explicitly asks you to.`);
196
197
  }
197
198
 
199
+ // CLAUDE.md parity — non-Claude providers don't read CLAUDE.md natively,
200
+ // so inject its project content (minus the GROOVE section) into introContext
201
+ 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);
208
+ }
209
+ }
210
+
198
211
  // Codebase structure injection — give agents instant orientation
199
212
  const structureSummary = this.daemon.indexer?.getStructureSummary();
200
213
  if (structureSummary) {
@@ -381,6 +394,35 @@ export class Introducer {
381
394
  return lines.join('\n') + memorySection;
382
395
  }
383
396
 
397
+ _loadClaudeMd(workingDir) {
398
+ // Walk up from agent workingDir to find CLAUDE.md
399
+ let dir = workingDir || this.daemon.projectDir;
400
+ let claudePath = null;
401
+ for (let i = 0; i < 5; i++) {
402
+ const candidate = resolve(dir, 'CLAUDE.md');
403
+ if (existsSync(candidate)) { claudePath = candidate; break; }
404
+ const parent = resolve(dir, '..');
405
+ if (parent === dir) break;
406
+ dir = parent;
407
+ }
408
+ if (!claudePath) return null;
409
+ try {
410
+ let content = readFileSync(claudePath, 'utf8').trim();
411
+ // Strip the GROOVE:START to GROOVE:END section to avoid duplicating coordination data
412
+ const startIdx = content.indexOf(GROOVE_SECTION_START);
413
+ const endIdx = content.indexOf(GROOVE_SECTION_END);
414
+ if (startIdx !== -1 && endIdx !== -1) {
415
+ content = (content.slice(0, startIdx) + content.slice(endIdx + GROOVE_SECTION_END.length)).trim();
416
+ }
417
+ if (content.length > 8000) {
418
+ content = content.slice(0, 8000) + '\n\n*(truncated — read full file for details)*';
419
+ }
420
+ return content || null;
421
+ } catch {
422
+ return null;
423
+ }
424
+ }
425
+
384
426
  loadArchitectureDoc() {
385
427
  const projectDir = this.daemon.projectDir;
386
428
  const candidates = [
@@ -92,6 +92,7 @@ Key behaviors:
92
92
  - Checking the Zustand store (stores/groove.js) for existing state before adding new state
93
93
  - Reading app.css for existing animations and utility classes before creating new ones
94
94
  When making visual changes, always read app.css for the color palette and existing patterns first.
95
+ NEVER open files in a browser (no "open index.html", "open http://...", "xdg-open"). GROOVE has its own preview system.
95
96
 
96
97
  `,
97
98
  backend: `You are a Backend agent. You build and modify daemon services, API endpoints, and system logic. Focus on:
@@ -114,6 +115,7 @@ CRITICAL — NEVER DO THESE:
114
115
  - NEVER kill the daemon process. No "kill <pid>", "pkill groove", "killall node", etc.
115
116
  - NEVER run "./promote.sh", "./promote-local.sh", or any publish/deploy script.
116
117
  - NEVER start long-running dev servers (vite dev, npm start, next dev, etc.).
118
+ - NEVER open files in a browser. No "open index.html", "open http://...", "xdg-open", or any command that launches a browser. GROOVE has its own preview system.
117
119
  - NEVER use 'git add -f' or 'git add --force' to bypass .gitignore. If a file is gitignored, it should stay gitignored. Only stage files that git tracks normally. If .gitignore prevents staging, report it in your output — do NOT force-add.
118
120
  - NEVER use 'git push --force' or 'git push -f'. Force-pushing can destroy shared history.
119
121
  - NEVER modify .gitignore to include files that were previously excluded.
@@ -226,7 +228,7 @@ For MODE 1 (team creation):
226
228
  "agents": [
227
229
  { "role": "frontend", "phase": 1, "scope": ["src/components/**", "src/views/**"], "prompt": "Build the frontend: [specific tasks]" },
228
230
  { "role": "backend", "phase": 1, "scope": ["src/api/**", "src/server/**"], "prompt": "Build the backend: [specific tasks]" },
229
- { "role": "fullstack", "phase": 2, "scope": [], "prompt": "QC Senior Dev: Audit all changes from phase 1 agents. Verify correctness, fix issues, run tests, verify the build compiles (npm run build). Do NOT start long-running dev servers. Commit all changes." }
231
+ { "role": "fullstack", "phase": 2, "scope": [], "prompt": "QC Senior Dev: Audit all changes from phase 1 agents. Verify correctness, fix issues, run tests, verify the build compiles (npm run build). Do NOT start long-running dev servers. Do NOT open files in a browser — no 'open' commands. Commit all changes." }
230
232
  ],
231
233
  "preview": {
232
234
  "kind": "dev-server",
@@ -701,6 +703,7 @@ For normal file edits within your scope, proceed without review.
701
703
 
702
704
  // ─── Agent Loop path (local models with built-in agentic runtime) ───
703
705
  if (provider.constructor.useAgentLoop) {
706
+ provider.normalizeConfig(spawnConfig);
704
707
  const loopConfig = provider.getLoopConfig(spawnConfig);
705
708
  logStream.write(`[${new Date().toISOString()}] GROOVE agent-loop: model=${loopConfig.model} api=${loopConfig.apiBase}\n`);
706
709
 
@@ -813,6 +816,7 @@ For normal file edits within your scope, proceed without review.
813
816
  integrationEnv = this.daemon.integrations.getSpawnEnv(config.integrations);
814
817
  }
815
818
 
819
+ provider.normalizeConfig(spawnConfig);
816
820
  const spawnCmd = provider.buildSpawnCommand(spawnConfig);
817
821
  const { command: rawCommand, args, env, stdin: stdinData, cwd: providerCwd } = spawnCmd;
818
822
  const command = resolveProviderCommand(agent.provider || config.provider) || rawCommand;
@@ -29,6 +29,10 @@ export class Provider {
29
29
  return false; // Default: no hot-swap, needs rotation
30
30
  }
31
31
 
32
+ normalizeConfig(config) {
33
+ return config;
34
+ }
35
+
32
36
  parseOutput(line) {
33
37
  return null;
34
38
  }
@@ -70,6 +70,14 @@ export class ClaudeCodeProvider extends Provider {
70
70
  return 'npm i -g @anthropic-ai/claude-code';
71
71
  }
72
72
 
73
+ normalizeConfig(config) {
74
+ if (typeof config.reasoningEffort === 'number') {
75
+ const e = config.reasoningEffort;
76
+ config.effort = e <= 20 ? 'none' : e <= 40 ? 'low' : e <= 60 ? 'medium' : e <= 80 ? 'high' : 'xhigh';
77
+ }
78
+ return config;
79
+ }
80
+
73
81
  buildSpawnCommand(agent) {
74
82
  // Claude Code interactive mode:
75
83
  // claude [options] [prompt]
@@ -107,28 +107,54 @@ export class CodexProvider extends Provider {
107
107
  }
108
108
  }
109
109
 
110
+ normalizeConfig(config) {
111
+ if (typeof config.reasoningEffort === 'number') {
112
+ config.reasoningEffort = config.reasoningEffort <= 33 ? 'low' : config.reasoningEffort <= 66 ? 'medium' : 'high';
113
+ }
114
+ if (typeof config.verbosity === 'number') {
115
+ config.verbosity = config.verbosity <= 33 ? 'low' : config.verbosity <= 66 ? 'medium' : 'high';
116
+ }
117
+ return config;
118
+ }
119
+
110
120
  buildSpawnCommand(agent) {
111
121
  const args = ['exec'];
112
122
 
113
123
  if (agent.model) args.push('--model', agent.model);
124
+ if (agent.reasoningEffort) args.push('--reasoning-effort', agent.reasoningEffort);
114
125
 
115
126
  args.push('--json');
116
127
  args.push('--dangerously-bypass-approvals-and-sandbox');
117
128
 
118
129
  if (agent.workingDir) args.push('-C', agent.workingDir);
119
130
 
120
- if (agent.prompt) args.push(agent.prompt);
131
+ const fullPrompt = this.buildFullPrompt(agent);
121
132
 
122
133
  this._currentModel = agent.model;
123
134
  this._sessionInputTokens = 0;
124
135
 
136
+ // Pipe prompt via stdin to avoid ARG_MAX with large introContext
125
137
  return {
126
138
  command: 'codex',
127
139
  args,
128
140
  env: agent.apiKey ? { OPENAI_API_KEY: agent.apiKey } : {},
141
+ stdin: fullPrompt || undefined,
129
142
  };
130
143
  }
131
144
 
145
+ buildFullPrompt(agent) {
146
+ const parts = [];
147
+ if (agent.introContext) parts.push(agent.introContext);
148
+ if (agent.prompt) parts.push(`## Your Task\n\n${agent.prompt}`);
149
+ if (agent.scope && agent.scope.length > 0) {
150
+ parts.push(
151
+ `## Scope Rules\n\nYou MUST only modify files matching these patterns: ${agent.scope.join(', ')}. ` +
152
+ `Do not touch files outside your scope — other agents own them.`
153
+ );
154
+ }
155
+ return parts.join('\n\n');
156
+ }
157
+
132
158
  buildHeadlessCommand(prompt, model) {
133
159
  const args = ['exec', '--json', prompt];
134
160
  if (model) args.push('--model', model);
@@ -244,7 +270,7 @@ export class CodexProvider extends Provider {
244
270
  try {
245
271
  event = JSON.parse(trimmed);
246
272
  } catch {
247
- return { type: 'activity', data: trimmed };
273
+ return null;
248
274
  }
249
275
 
250
276
  switch (event.type) {
@@ -345,8 +371,11 @@ export class CodexProvider extends Provider {
345
371
  const outputTokens = usage.output_tokens || 0;
346
372
  const cachedTokens = usage.cached_input_tokens || 0;
347
373
  const reasoningTokens = usage.output_tokens_details?.reasoning_tokens || 0;
348
- const totalTokens = inputTokens + outputTokens;
349
- const cacheCreationTokens = cachedTokens > 0 ? Math.max(0, inputTokens - cachedTokens) : 0;
374
+ // OpenAI includes cached tokens IN input_tokens; Anthropic does not.
375
+ // Subtract cached to get new-processing-only count, matching Claude's convention.
376
+ const newInputTokens = Math.max(0, inputTokens - cachedTokens);
377
+ const totalTokens = newInputTokens + outputTokens;
378
+ const cacheCreationTokens = cachedTokens > 0 ? newInputTokens : 0;
350
379
 
351
380
  const model = CodexProvider.models.find((m) => m.id === this._currentModel);
352
381
  const pricing = model?.pricing;
@@ -67,10 +67,23 @@ export class GeminiProvider extends Provider {
67
67
  command: 'gemini',
68
68
  args,
69
69
  env: agent.apiKey ? { GEMINI_API_KEY: agent.apiKey } : {},
70
- stdin: agent.prompt || undefined,
70
+ stdin: this.buildFullPrompt(agent) || undefined,
71
71
  };
72
72
  }
73
73
 
74
+ buildFullPrompt(agent) {
75
+ const parts = [];
76
+ if (agent.introContext) parts.push(agent.introContext);
77
+ if (agent.prompt) parts.push(`## Your Task\n\n${agent.prompt}`);
78
+ if (agent.scope && agent.scope.length > 0) {
79
+ parts.push(
80
+ `## Scope Rules\n\nYou MUST only modify files matching these patterns: ${agent.scope.join(', ')}. ` +
81
+ `Do not touch files outside your scope — other agents own them.`
82
+ );
83
+ }
84
+ return parts.join('\n\n');
85
+ }
86
+
74
87
  buildHeadlessCommand(prompt, model) {
75
88
  const args = ['--output-format', 'stream-json', '-p', prompt];
76
89
  if (model) args.push('--model', model);
@@ -55,12 +55,19 @@ export class GrokProvider extends Provider {
55
55
  return null; // No CLI
56
56
  }
57
57
 
58
+ normalizeConfig(config) {
59
+ if (typeof config.temperature !== 'number' && typeof config.reasoningEffort === 'number') {
60
+ config.temperature = 0.1 + (100 - config.reasoningEffort) * 0.008;
61
+ }
62
+ return config;
63
+ }
64
+
58
65
  getLoopConfig(agent) {
59
66
  return {
60
67
  apiBase: 'https://api.x.ai/v1',
61
68
  model: agent.model,
62
69
  contextWindow: 131072,
63
- temperature: 0.1,
70
+ temperature: typeof agent.temperature === 'number' ? agent.temperature : 0.1,
64
71
  maxResponseTokens: 16384,
65
72
  stream: true,
66
73
  apiKey: agent.apiKey,
@@ -127,6 +127,13 @@ export class LocalProvider extends Provider {
127
127
  * Get configuration for the agent loop runtime.
128
128
  * Called by ProcessManager when useAgentLoop is true.
129
129
  */
130
+ normalizeConfig(config) {
131
+ if (typeof config.temperature !== 'number' && typeof config.reasoningEffort === 'number') {
132
+ config.temperature = 0.1 + (100 - config.reasoningEffort) * 0.008;
133
+ }
134
+ return config;
135
+ }
136
+
130
137
  getLoopConfig(agent) {
131
138
  const model = agent.model || 'qwen2.5-coder:7b';
132
139
  const contextWindow = this.getContextWindow(model);
@@ -143,7 +150,7 @@ export class LocalProvider extends Provider {
143
150
  apiBase,
144
151
  model,
145
152
  contextWindow,
146
- temperature: 0.1,
153
+ temperature: typeof agent.temperature === 'number' ? agent.temperature : 0.1,
147
154
  maxResponseTokens: 4096,
148
155
  stream: true,
149
156
  headers: {},