groove-dev 0.27.89 → 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.
- package/moe-training/client/parsers/claude-code.js +0 -2
- package/moe-training/client/session-attestation.js +2 -1
- package/moe-training/client/trajectory-capture.js +6 -0
- package/moe-training/test/client/parsers/claude-code.test.js +2 -2
- 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 +244 -12
- package/node_modules/@groove-dev/daemon/src/conversations.js +32 -6
- package/node_modules/@groove-dev/daemon/src/introducer.js +42 -0
- package/node_modules/@groove-dev/daemon/src/process.js +5 -1
- package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +9 -1
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +34 -5
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +15 -2
- package/node_modules/@groove-dev/daemon/src/providers/grok.js +10 -3
- package/node_modules/@groove-dev/daemon/src/providers/local.js +8 -1
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +74 -5
- package/node_modules/@groove-dev/daemon/src/validate.js +22 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-Bo6AeNmM.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DWv32qyJ.js +8653 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -44
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +29 -28
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +53 -143
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +3 -30
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +163 -153
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +16 -8
- package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +26 -17
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +29 -23
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +5 -1
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +9 -5
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +5 -1
- package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +50 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +145 -9
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +707 -14
- 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 +244 -12
- package/packages/daemon/src/conversations.js +32 -6
- package/packages/daemon/src/introducer.js +42 -0
- package/packages/daemon/src/process.js +5 -1
- package/packages/daemon/src/providers/base.js +4 -0
- package/packages/daemon/src/providers/claude-code.js +9 -1
- package/packages/daemon/src/providers/codex.js +34 -5
- package/packages/daemon/src/providers/gemini.js +15 -2
- package/packages/daemon/src/providers/grok.js +10 -3
- package/packages/daemon/src/providers/local.js +8 -1
- package/packages/daemon/src/tunnel-manager.js +74 -5
- package/packages/daemon/src/validate.js +22 -1
- package/packages/gui/dist/assets/index-Bo6AeNmM.css +1 -0
- package/packages/gui/dist/assets/index-DWv32qyJ.js +8653 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-chat.jsx +26 -44
- package/packages/gui/src/components/agents/agent-file-tree.jsx +29 -28
- package/packages/gui/src/components/agents/workspace-mode.jsx +53 -143
- package/packages/gui/src/components/chat/chat-header.jsx +3 -30
- package/packages/gui/src/components/chat/chat-input.jsx +163 -153
- package/packages/gui/src/components/chat/chat-view.jsx +16 -8
- package/packages/gui/src/components/chat/conversation-list.jsx +26 -17
- package/packages/gui/src/components/editor/code-editor.jsx +29 -23
- package/packages/gui/src/components/settings/quick-connect.jsx +5 -1
- package/packages/gui/src/components/settings/remote-server-card.jsx +9 -5
- package/packages/gui/src/components/settings/ssh-wizard.jsx +5 -1
- package/packages/gui/src/components/ui/slider.jsx +50 -0
- package/packages/gui/src/stores/groove.js +145 -9
- package/packages/gui/src/views/agents.jsx +707 -14
- package/workspace.png +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BKD8JAsV.js +0 -8642
- package/node_modules/@groove-dev/gui/dist/assets/index-D4vJ_1ET.css +0 -1
- package/packages/gui/dist/assets/index-BKD8JAsV.js +0 -8642
- package/packages/gui/dist/assets/index-D4vJ_1ET.css +0 -1
|
@@ -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(
|
|
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,
|
|
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,
|
|
82
|
+
assert.equal(result.token_count, undefined);
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
it('extracts tokens from assistant event', () => {
|
|
@@ -1115,8 +1115,8 @@ export function createApi(app, daemon) {
|
|
|
1115
1115
|
app.post('/api/conversations', async (req, res) => {
|
|
1116
1116
|
try {
|
|
1117
1117
|
const { provider, model, title, mode, reasoning_effort, verbosity } = req.body;
|
|
1118
|
-
if (
|
|
1119
|
-
return res.status(400).json({ error: 'provider
|
|
1118
|
+
if (provider && typeof provider !== 'string') {
|
|
1119
|
+
return res.status(400).json({ error: 'provider must be a string' });
|
|
1120
1120
|
}
|
|
1121
1121
|
if (mode && mode !== 'api' && mode !== 'agent') {
|
|
1122
1122
|
return res.status(400).json({ error: 'mode must be "api" or "agent"' });
|
|
@@ -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
|
-
|
|
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() || [] });
|
|
@@ -4504,8 +4729,10 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4504
4729
|
}
|
|
4505
4730
|
|
|
4506
4731
|
daemon.config.defaultProvider = provider;
|
|
4732
|
+
daemon.config.defaultChatProvider = provider;
|
|
4507
4733
|
if (model && typeof model === 'string' && model.length <= 100) {
|
|
4508
4734
|
daemon.config.defaultModel = model.trim();
|
|
4735
|
+
daemon.config.defaultChatModel = model.trim();
|
|
4509
4736
|
}
|
|
4510
4737
|
const { saveConfig } = await import('./firstrun.js');
|
|
4511
4738
|
saveConfig(daemon.grooveDir, daemon.config);
|
|
@@ -4537,15 +4764,20 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4537
4764
|
saveConfig(daemon.grooveDir, daemon.config);
|
|
4538
4765
|
|
|
4539
4766
|
if (enabled) {
|
|
4540
|
-
const userId = ConsentManager.getOrCreateUserId();
|
|
4541
|
-
const consent = new ConsentManager();
|
|
4542
4767
|
try {
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4768
|
+
const userId = ConsentManager.getOrCreateUserId();
|
|
4769
|
+
const consent = new ConsentManager();
|
|
4770
|
+
try {
|
|
4771
|
+
consent.recordConsent(userId, true, '1.0');
|
|
4772
|
+
} finally {
|
|
4773
|
+
consent.close();
|
|
4774
|
+
}
|
|
4775
|
+
await daemon._initTrajectoryCapture();
|
|
4776
|
+
daemon.state.set('training_enrolled_at', new Date().toISOString());
|
|
4777
|
+
} catch (e) {
|
|
4778
|
+
daemon.config.training_opt_in = false;
|
|
4779
|
+
return res.status(500).json({ error: 'Failed to enable data sharing', detail: e.message });
|
|
4546
4780
|
}
|
|
4547
|
-
await daemon._initTrajectoryCapture();
|
|
4548
|
-
daemon.state.set('training_enrolled_at', new Date().toISOString());
|
|
4549
4781
|
} else {
|
|
4550
4782
|
if (daemon.trajectoryCapture) {
|
|
4551
4783
|
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
|
|
332
|
-
|
|
333
|
-
if (!
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
@@ -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;
|
|
@@ -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]
|
|
@@ -282,7 +290,7 @@ export class ClaudeCodeProvider extends Provider {
|
|
|
282
290
|
let finished = false;
|
|
283
291
|
const finish = () => { if (!finished) { finished = true; onDone(); } };
|
|
284
292
|
const body = JSON.stringify({
|
|
285
|
-
model
|
|
293
|
+
model,
|
|
286
294
|
messages,
|
|
287
295
|
max_tokens: 8192,
|
|
288
296
|
stream: true,
|