groove-dev 0.27.119 → 0.27.120
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/trajectory-capture.js +55 -0
- package/moe-training/test/client/trajectory-capture.test.js +63 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/start.js +2 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +30 -10
- package/node_modules/@groove-dev/daemon/src/conversations.js +54 -32
- package/node_modules/@groove-dev/daemon/src/index.js +2 -1
- package/node_modules/@groove-dev/daemon/src/introducer.js +45 -20
- package/node_modules/@groove-dev/daemon/src/process.js +47 -1
- package/node_modules/@groove-dev/daemon/src/teams.js +33 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-DT6Jbf_q.css → index-BLd3MON8.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-BxPCaxlC.js → index-oKbzuMnX.js} +1721 -1721
- 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-config.jsx +3 -41
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +4 -43
- package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +8 -23
- package/node_modules/@groove-dev/gui/src/components/settings/ProviderSetupWizard.jsx +54 -143
- package/node_modules/@groove-dev/gui/src/components/ui/data-sharing-modal.jsx +7 -57
- package/node_modules/@groove-dev/gui/src/stores/groove.js +13 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +50 -84
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +61 -1
- package/node_modules/moe-training/client/trajectory-capture.js +55 -0
- package/node_modules/moe-training/test/client/trajectory-capture.test.js +63 -0
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/start.js +2 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +30 -10
- package/packages/daemon/src/conversations.js +54 -32
- package/packages/daemon/src/index.js +2 -1
- package/packages/daemon/src/introducer.js +45 -20
- package/packages/daemon/src/process.js +47 -1
- package/packages/daemon/src/teams.js +33 -0
- package/packages/gui/dist/assets/{index-DT6Jbf_q.css → index-BLd3MON8.css} +1 -1
- package/packages/gui/dist/assets/{index-BxPCaxlC.js → index-oKbzuMnX.js} +1721 -1721
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-config.jsx +3 -41
- package/packages/gui/src/components/agents/spawn-wizard.jsx +4 -43
- package/packages/gui/src/components/onboarding/setup-wizard.jsx +8 -23
- package/packages/gui/src/components/settings/ProviderSetupWizard.jsx +54 -143
- package/packages/gui/src/components/ui/data-sharing-modal.jsx +7 -57
- package/packages/gui/src/stores/groove.js +13 -0
- package/packages/gui/src/views/settings.jsx +50 -84
- package/packages/gui/src/views/teams.jsx +61 -1
|
@@ -128,6 +128,61 @@ export class TrajectoryCapture {
|
|
|
128
128
|
await this._attestation.openSession(sessionId, metadata);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
onChatTurnStart(conversationId, provider, model, message) {
|
|
132
|
+
if (!this._enabled) return null;
|
|
133
|
+
|
|
134
|
+
const agentId = `chat-api-${conversationId}-${Date.now()}`;
|
|
135
|
+
const sessionId = `sess_${randomUUID()}`;
|
|
136
|
+
const contributorId = ConsentManager.getOrCreateUserId();
|
|
137
|
+
const metadata = {
|
|
138
|
+
model_engine: model,
|
|
139
|
+
provider,
|
|
140
|
+
agent_role: 'chat',
|
|
141
|
+
agent_id: agentId,
|
|
142
|
+
task_complexity: 'medium',
|
|
143
|
+
team_size: 1,
|
|
144
|
+
session_quality: 0,
|
|
145
|
+
groove_version: this._grooveVersion,
|
|
146
|
+
leaf_context: null,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const builder = new EnvelopeBuilder(sessionId, contributorId, metadata);
|
|
150
|
+
const classifier = new StepClassifier();
|
|
151
|
+
|
|
152
|
+
const ctx = {
|
|
153
|
+
sessionId,
|
|
154
|
+
parser: null,
|
|
155
|
+
builder,
|
|
156
|
+
classifier,
|
|
157
|
+
metadata,
|
|
158
|
+
stepCount: 0,
|
|
159
|
+
chunkCount: 0,
|
|
160
|
+
totalTokens: 0,
|
|
161
|
+
errorsEncountered: 0,
|
|
162
|
+
errorsRecovered: 0,
|
|
163
|
+
filesModified: 0,
|
|
164
|
+
coordinationEvents: 0,
|
|
165
|
+
startTime: Date.now(),
|
|
166
|
+
chunkTimer: null,
|
|
167
|
+
allSteps: [],
|
|
168
|
+
revisionRounds: 0,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
this._contexts.set(agentId, ctx);
|
|
172
|
+
|
|
173
|
+
if (message && typeof message === 'string' && message.trim()) {
|
|
174
|
+
this._processStep(agentId, ctx, {
|
|
175
|
+
type: 'instruction',
|
|
176
|
+
content: message.slice(0, USER_MESSAGE_MAX_CHARS),
|
|
177
|
+
source: 'user',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this._attestation.openSession(sessionId, metadata).catch(() => {});
|
|
182
|
+
|
|
183
|
+
return agentId;
|
|
184
|
+
}
|
|
185
|
+
|
|
131
186
|
onStdoutLine(agentId, jsonLine) {
|
|
132
187
|
if (!this._enabled) return;
|
|
133
188
|
const ctx = this._contexts.get(agentId);
|
|
@@ -354,6 +354,69 @@ describe('TrajectoryCapture — planner/conversational eligibility', () => {
|
|
|
354
354
|
});
|
|
355
355
|
});
|
|
356
356
|
|
|
357
|
+
describe('TrajectoryCapture — API chat capture via onChatTurnStart', () => {
|
|
358
|
+
function makeChatTc() {
|
|
359
|
+
const tc = makeTc();
|
|
360
|
+
tc._enabled = true;
|
|
361
|
+
tc._scrubber = { scrub: (s) => s };
|
|
362
|
+
tc._attestation = { openSession: async () => {}, closeSession: async () => {}, signEnvelope: (sid, e) => e };
|
|
363
|
+
tc._transmissionQueue = { enqueue: () => {}, waitForDrain: async () => {} };
|
|
364
|
+
tc._domainTagger = null;
|
|
365
|
+
return tc;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
it('returns a synthetic agent ID and creates context', () => {
|
|
369
|
+
const tc = makeChatTc();
|
|
370
|
+
const agentId = tc.onChatTurnStart('conv-123', 'claude-code', 'opus', 'What is React?');
|
|
371
|
+
assert.ok(agentId);
|
|
372
|
+
assert.ok(agentId.startsWith('chat-api-conv-123-'));
|
|
373
|
+
const ctx = tc._contexts.get(agentId);
|
|
374
|
+
assert.ok(ctx);
|
|
375
|
+
assert.equal(ctx.metadata.agent_role, 'chat');
|
|
376
|
+
assert.equal(ctx.metadata.provider, 'claude-code');
|
|
377
|
+
assert.equal(ctx.metadata.model_engine, 'opus');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('records the user message as an instruction step', () => {
|
|
381
|
+
const tc = makeChatTc();
|
|
382
|
+
const agentId = tc.onChatTurnStart('conv-456', 'claude-code', 'opus', 'Explain hooks');
|
|
383
|
+
const ctx = tc._contexts.get(agentId);
|
|
384
|
+
assert.equal(ctx.stepCount, 1);
|
|
385
|
+
assert.equal(ctx.allSteps[0].type, 'instruction');
|
|
386
|
+
assert.ok(ctx.allSteps[0].content.includes('Explain hooks'));
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('works with onParsedOutput and onAgentComplete', async () => {
|
|
390
|
+
const tc = makeChatTc();
|
|
391
|
+
const agentId = tc.onChatTurnStart('conv-789', 'claude-code', 'opus', 'Tell me about React');
|
|
392
|
+
|
|
393
|
+
tc.onParsedOutput(agentId, { type: 'activity', subtype: 'assistant', data: 'React is a UI library' });
|
|
394
|
+
tc.onParsedOutput(agentId, { type: 'result', data: 'React is a UI library' });
|
|
395
|
+
|
|
396
|
+
const ctx = tc._contexts.get(agentId);
|
|
397
|
+
assert.equal(ctx.stepCount, 3);
|
|
398
|
+
assert.equal(ctx.allSteps[1].type, 'thought');
|
|
399
|
+
assert.equal(ctx.allSteps[2].type, 'resolution');
|
|
400
|
+
|
|
401
|
+
await tc.onAgentComplete(agentId, { status: 'SUCCESS' });
|
|
402
|
+
assert.equal(tc._contexts.has(agentId), false);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('returns null when disabled', () => {
|
|
406
|
+
const tc = makeChatTc();
|
|
407
|
+
tc._enabled = false;
|
|
408
|
+
const agentId = tc.onChatTurnStart('conv-000', 'claude-code', 'opus', 'Hello');
|
|
409
|
+
assert.equal(agentId, null);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('context has no parser (not needed for API chat)', () => {
|
|
413
|
+
const tc = makeChatTc();
|
|
414
|
+
const agentId = tc.onChatTurnStart('conv-nop', 'claude-code', 'opus', 'Hello');
|
|
415
|
+
const ctx = tc._contexts.get(agentId);
|
|
416
|
+
assert.equal(ctx.parser, null);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
357
420
|
describe('TrajectoryCapture — initial prompt capture', () => {
|
|
358
421
|
function makeSpawnTc() {
|
|
359
422
|
const tc = makeTc();
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
5
|
import { resolve } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
6
7
|
import { Daemon } from '@groove-dev/daemon';
|
|
7
8
|
import chalk from 'chalk';
|
|
8
9
|
import { runSetupWizard, saveKeysViaDaemon } from '../setup.js';
|
|
9
10
|
|
|
10
11
|
export async function start(options) {
|
|
11
|
-
const grooveDir = process.env.GROOVE_DIR || resolve(
|
|
12
|
+
const grooveDir = process.env.GROOVE_DIR || resolve(homedir(), '.groove');
|
|
12
13
|
const isFirstRun = !existsSync(resolve(grooveDir, 'config.json'));
|
|
13
14
|
|
|
14
15
|
// ── First-run interactive wizard ────────────────────────────
|
|
@@ -1147,6 +1147,16 @@ export function createApi(app, daemon) {
|
|
|
1147
1147
|
}
|
|
1148
1148
|
});
|
|
1149
1149
|
|
|
1150
|
+
app.post('/api/teams/:id/promote', (req, res) => {
|
|
1151
|
+
try {
|
|
1152
|
+
const team = daemon.teams.promote(req.params.id);
|
|
1153
|
+
daemon.audit.log('team.promote', { id: team.id, name: team.name });
|
|
1154
|
+
res.json(team);
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
res.status(400).json({ error: err.message });
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1150
1160
|
// --- Conversations ---
|
|
1151
1161
|
|
|
1152
1162
|
app.get('/api/conversations', (req, res) => {
|
|
@@ -2957,8 +2967,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2957
2967
|
|
|
2958
2968
|
try {
|
|
2959
2969
|
const stat = statSync(result.fullPath);
|
|
2960
|
-
if (stat.size >
|
|
2961
|
-
return res.status(400).json({ error: 'File too large (>
|
|
2970
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
2971
|
+
return res.status(400).json({ error: 'File too large (>50MB)' });
|
|
2962
2972
|
}
|
|
2963
2973
|
|
|
2964
2974
|
// Binary detection: check first 8KB for null bytes
|
|
@@ -2990,8 +3000,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2990
3000
|
if (typeof content !== 'string') {
|
|
2991
3001
|
return res.status(400).json({ error: 'content must be a string' });
|
|
2992
3002
|
}
|
|
2993
|
-
if (content.length >
|
|
2994
|
-
return res.status(400).json({ error: 'Content too large (>
|
|
3003
|
+
if (content.length > 50 * 1024 * 1024) {
|
|
3004
|
+
return res.status(400).json({ error: 'Content too large (>50MB)' });
|
|
2995
3005
|
}
|
|
2996
3006
|
|
|
2997
3007
|
try {
|
|
@@ -3435,6 +3445,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3435
3445
|
const plannerAgent = found.agentId ? daemon.registry.get(found.agentId) : null;
|
|
3436
3446
|
const baseDir = plannerAgent?.workingDir || daemon.config?.defaultWorkingDir || daemon.projectDir;
|
|
3437
3447
|
const plannerProvider = plannerAgent?.provider || undefined;
|
|
3448
|
+
const plannerModel = plannerAgent?.model || undefined;
|
|
3438
3449
|
|
|
3439
3450
|
// Use the planner's teamId so launched agents join the correct team.
|
|
3440
3451
|
// Priority: explicit from frontend > agent that wrote the file > most recent planner > default
|
|
@@ -3477,6 +3488,15 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3477
3488
|
});
|
|
3478
3489
|
}
|
|
3479
3490
|
|
|
3491
|
+
function resolveProviderAndModel(cfgProvider, cfgModel, fallbackProvider, fallbackModel) {
|
|
3492
|
+
const provider = cfgProvider || plannerProvider || daemon.config?.defaultProvider || fallbackProvider || undefined;
|
|
3493
|
+
if (cfgModel) return { provider, model: cfgModel };
|
|
3494
|
+
if (!cfgProvider && plannerProvider && plannerProvider !== daemon.config?.defaultProvider) {
|
|
3495
|
+
return { provider, model: plannerModel || 'auto' };
|
|
3496
|
+
}
|
|
3497
|
+
return { provider, model: daemon.config?.defaultModel || fallbackModel || 'auto' };
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3480
3500
|
// Team-level overrides from the pre-planner config panel
|
|
3481
3501
|
const teamProvider = req.body?.teamProvider || undefined;
|
|
3482
3502
|
const teamModel = req.body?.teamModel || undefined;
|
|
@@ -3500,10 +3520,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3500
3520
|
|
|
3501
3521
|
// Safety net: if planner forgot the QC agent, auto-add one
|
|
3502
3522
|
if (phase2.length === 0 && phase1.length >= 2) {
|
|
3523
|
+
const { provider: qcProvider, model: qcModel } = resolveProviderAndModel(teamProvider, teamModel);
|
|
3503
3524
|
phase2 = [{
|
|
3504
3525
|
name: 'qc-agent',
|
|
3505
3526
|
role: 'fullstack', phase: 2, scope: [],
|
|
3506
|
-
provider:
|
|
3527
|
+
provider: qcProvider,
|
|
3528
|
+
model: qcModel,
|
|
3507
3529
|
prompt: 'QC Senior Dev: All builder agents have completed. Audit their changes for correctness, fix any issues, run tests, and verify the project builds cleanly (npm run build). Do NOT start long-running dev servers — just verify the build succeeds. Commit all changes. IMPORTANT: Do NOT delete files from other projects or directories outside this project.',
|
|
3508
3530
|
}];
|
|
3509
3531
|
}
|
|
@@ -3556,8 +3578,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3556
3578
|
role: existing.role,
|
|
3557
3579
|
scope: normalizeScope(config.scope || existing.scope || [], existing.workingDir || projectWorkingDir),
|
|
3558
3580
|
prompt,
|
|
3559
|
-
|
|
3560
|
-
model: config.model || existing.model || daemon.config?.defaultModel || 'auto',
|
|
3581
|
+
...resolveProviderAndModel(config.provider, config.model, existing.provider, existing.model),
|
|
3561
3582
|
permission: config.permission || existing.permission || 'auto',
|
|
3562
3583
|
workingDir: existing.workingDir || projectWorkingDir,
|
|
3563
3584
|
name: existing.name,
|
|
@@ -3581,8 +3602,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3581
3602
|
role: config.role,
|
|
3582
3603
|
scope: normalizeScope(config.scope || [], config.workingDir || projectWorkingDir),
|
|
3583
3604
|
prompt,
|
|
3584
|
-
|
|
3585
|
-
model: config.model || daemon.config?.defaultModel || 'auto',
|
|
3605
|
+
...resolveProviderAndModel(config.provider, config.model),
|
|
3586
3606
|
permission: config.permission || 'auto',
|
|
3587
3607
|
workingDir: config.workingDir || projectWorkingDir,
|
|
3588
3608
|
name: config.name || undefined,
|
|
@@ -3624,7 +3644,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3624
3644
|
waitFor: phase1Ids,
|
|
3625
3645
|
agents: phase2.map((c) => ({
|
|
3626
3646
|
role: c.role, scope: c.scope || [], prompt: c.prompt || '',
|
|
3627
|
-
|
|
3647
|
+
...resolveProviderAndModel(c.provider, c.model),
|
|
3628
3648
|
permission: c.permission || 'auto',
|
|
3629
3649
|
reasoningEffort: c.reasoningEffort, temperature: c.temperature, verbosity: c.verbosity,
|
|
3630
3650
|
workingDir: c.workingDir || projectWorkingDir,
|
|
@@ -387,10 +387,19 @@ export class ConversationManager {
|
|
|
387
387
|
const effectiveReasoningEffort = reasoningEffort || conv.reasoningEffort || null;
|
|
388
388
|
const effectiveVerbosity = verbosity || conv.verbosity || null;
|
|
389
389
|
|
|
390
|
+
// Trajectory capture for training data
|
|
391
|
+
const tc = this.daemon.trajectoryCapture;
|
|
392
|
+
let tcAgentId = null;
|
|
393
|
+
let tcResponseText = '';
|
|
394
|
+
if (tc) {
|
|
395
|
+
try { tcAgentId = tc.onChatTurnStart(id, providerName, modelId, message); } catch { /* never block chat */ }
|
|
396
|
+
}
|
|
397
|
+
|
|
390
398
|
// Try direct API streaming first (sub-second latency)
|
|
391
399
|
const controller = provider.streamChat(
|
|
392
400
|
messages, modelId, apiKey,
|
|
393
401
|
(text) => {
|
|
402
|
+
if (tcAgentId) tcResponseText += text;
|
|
394
403
|
this.daemon.broadcast({
|
|
395
404
|
type: 'conversation:chunk',
|
|
396
405
|
data: { conversationId: id, text },
|
|
@@ -402,6 +411,15 @@ export class ConversationManager {
|
|
|
402
411
|
this._save();
|
|
403
412
|
}
|
|
404
413
|
this._getStreamingProcesses().delete(id);
|
|
414
|
+
if (tcAgentId && tc) {
|
|
415
|
+
try {
|
|
416
|
+
tc.onParsedOutput(tcAgentId, { type: 'activity', subtype: 'assistant', data: tcResponseText });
|
|
417
|
+
tc.onParsedOutput(tcAgentId, { type: 'result', data: tcResponseText });
|
|
418
|
+
tc.onAgentComplete(tcAgentId, { status: 'SUCCESS' });
|
|
419
|
+
const count = (this.daemon.state.get('training_sessions_captured') || 0) + 1;
|
|
420
|
+
this.daemon.state.set('training_sessions_captured', count);
|
|
421
|
+
} catch { /* never block chat */ }
|
|
422
|
+
}
|
|
405
423
|
this.daemon.broadcast({
|
|
406
424
|
type: 'conversation:complete',
|
|
407
425
|
data: { conversationId: id },
|
|
@@ -409,6 +427,9 @@ export class ConversationManager {
|
|
|
409
427
|
},
|
|
410
428
|
(err) => {
|
|
411
429
|
this._getStreamingProcesses().delete(id);
|
|
430
|
+
if (tcAgentId && tc) {
|
|
431
|
+
try { tc.onAgentCrash(tcAgentId, err); } catch { /* never block chat */ }
|
|
432
|
+
}
|
|
412
433
|
this.daemon.broadcast({
|
|
413
434
|
type: 'conversation:error',
|
|
414
435
|
data: { conversationId: id, error: err.message },
|
|
@@ -430,6 +451,9 @@ export class ConversationManager {
|
|
|
430
451
|
const prompt = this._buildHistoryPrompt(history, message);
|
|
431
452
|
const headlessCmd = provider.buildHeadlessCommand(prompt, modelId);
|
|
432
453
|
if (!headlessCmd) {
|
|
454
|
+
if (tcAgentId && tc) {
|
|
455
|
+
try { tc.onAgentCrash(tcAgentId, new Error('No API key for chat')); } catch { /* never block chat */ }
|
|
456
|
+
}
|
|
433
457
|
this.daemon.broadcast({
|
|
434
458
|
type: 'conversation:error',
|
|
435
459
|
data: { conversationId: id, error: `${providerName} requires an API key for chat` },
|
|
@@ -450,6 +474,9 @@ export class ConversationManager {
|
|
|
450
474
|
|
|
451
475
|
proc.on('error', (err) => {
|
|
452
476
|
this._getStreamingProcesses().delete(id);
|
|
477
|
+
if (tcAgentId && tc) {
|
|
478
|
+
try { tc.onAgentCrash(tcAgentId, err); } catch { /* never block chat */ }
|
|
479
|
+
}
|
|
453
480
|
this.daemon.broadcast({
|
|
454
481
|
type: 'conversation:error',
|
|
455
482
|
data: { conversationId: id, error: err.message },
|
|
@@ -461,6 +488,14 @@ export class ConversationManager {
|
|
|
461
488
|
proc.stdin.end();
|
|
462
489
|
}
|
|
463
490
|
|
|
491
|
+
const emitChunk = (text) => {
|
|
492
|
+
if (tcAgentId) tcResponseText += text;
|
|
493
|
+
this.daemon.broadcast({
|
|
494
|
+
type: 'conversation:chunk',
|
|
495
|
+
data: { conversationId: id, text },
|
|
496
|
+
});
|
|
497
|
+
};
|
|
498
|
+
|
|
464
499
|
proc.stdout.on('data', (data) => {
|
|
465
500
|
const text = data.toString();
|
|
466
501
|
const lines = text.split('\n');
|
|
@@ -473,64 +508,51 @@ export class ConversationManager {
|
|
|
473
508
|
if (json.type === 'assistant' && json.message?.content) {
|
|
474
509
|
for (const block of json.message.content) {
|
|
475
510
|
if (block.type === 'text' && block.text) {
|
|
476
|
-
|
|
477
|
-
type: 'conversation:chunk',
|
|
478
|
-
data: { conversationId: id, text: block.text },
|
|
479
|
-
});
|
|
511
|
+
emitChunk(block.text);
|
|
480
512
|
}
|
|
481
513
|
}
|
|
482
514
|
continue;
|
|
483
515
|
}
|
|
484
516
|
if (json.type === 'content_block_delta' && json.delta?.text) {
|
|
485
|
-
|
|
486
|
-
type: 'conversation:chunk',
|
|
487
|
-
data: { conversationId: id, text: json.delta.text },
|
|
488
|
-
});
|
|
517
|
+
emitChunk(json.delta.text);
|
|
489
518
|
continue;
|
|
490
519
|
}
|
|
491
520
|
if (json.type === 'result' && json.result) continue;
|
|
492
521
|
if (json.type === 'token' && json.text != null) {
|
|
493
|
-
|
|
494
|
-
type: 'conversation:chunk',
|
|
495
|
-
data: { conversationId: id, text: json.text },
|
|
496
|
-
});
|
|
522
|
+
emitChunk(json.text);
|
|
497
523
|
continue;
|
|
498
524
|
}
|
|
499
525
|
if ((json.type === 'done' || json.type === 'complete' || json.type === 'result') && json.text) {
|
|
500
|
-
|
|
501
|
-
type: 'conversation:chunk',
|
|
502
|
-
data: { conversationId: id, text: json.text },
|
|
503
|
-
});
|
|
526
|
+
emitChunk(json.text);
|
|
504
527
|
continue;
|
|
505
528
|
}
|
|
506
529
|
if (json.content?.[0]?.text) {
|
|
507
|
-
|
|
508
|
-
type: 'conversation:chunk',
|
|
509
|
-
data: { conversationId: id, text: json.content[0].text },
|
|
510
|
-
});
|
|
530
|
+
emitChunk(json.content[0].text);
|
|
511
531
|
continue;
|
|
512
532
|
}
|
|
513
533
|
} catch { /* not JSON */ }
|
|
514
534
|
|
|
515
535
|
if (!trimmed.startsWith('{')) {
|
|
516
|
-
|
|
517
|
-
type: 'conversation:chunk',
|
|
518
|
-
data: { conversationId: id, text: trimmed },
|
|
519
|
-
});
|
|
536
|
+
emitChunk(trimmed);
|
|
520
537
|
}
|
|
521
538
|
}
|
|
522
539
|
});
|
|
523
540
|
|
|
524
|
-
proc.on('error', (err) => {
|
|
525
|
-
this._getStreamingProcesses().delete(id);
|
|
526
|
-
this.daemon.broadcast({
|
|
527
|
-
type: 'conversation:error',
|
|
528
|
-
data: { conversationId: id, error: err.message },
|
|
529
|
-
});
|
|
530
|
-
});
|
|
531
|
-
|
|
532
541
|
proc.on('exit', (code) => {
|
|
533
542
|
this._getStreamingProcesses().delete(id);
|
|
543
|
+
if (tcAgentId && tc) {
|
|
544
|
+
try {
|
|
545
|
+
tc.onParsedOutput(tcAgentId, { type: 'activity', subtype: 'assistant', data: tcResponseText });
|
|
546
|
+
tc.onParsedOutput(tcAgentId, { type: 'result', data: tcResponseText });
|
|
547
|
+
if (code === 0 || code === null) {
|
|
548
|
+
tc.onAgentComplete(tcAgentId, { status: 'SUCCESS' });
|
|
549
|
+
} else {
|
|
550
|
+
tc.onAgentCrash(tcAgentId, new Error(`Exit code ${code}`));
|
|
551
|
+
}
|
|
552
|
+
const count = (this.daemon.state.get('training_sessions_captured') || 0) + 1;
|
|
553
|
+
this.daemon.state.set('training_sessions_captured', count);
|
|
554
|
+
} catch { /* never block chat */ }
|
|
555
|
+
}
|
|
534
556
|
this.daemon.broadcast({
|
|
535
557
|
type: 'conversation:complete',
|
|
536
558
|
data: { conversationId: id, exitCode: code },
|
|
@@ -5,6 +5,7 @@ import { createServer as createHttpServer, request as httpProxyRequest } from 'h
|
|
|
5
5
|
import { createServer as createNetServer } from 'net';
|
|
6
6
|
import { execFileSync } from 'child_process';
|
|
7
7
|
import { resolve } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
8
9
|
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, readdirSync, rmdirSync, rmSync, statSync } from 'fs';
|
|
9
10
|
import express from 'express';
|
|
10
11
|
import { WebSocketServer } from 'ws';
|
|
@@ -94,7 +95,7 @@ export class Daemon {
|
|
|
94
95
|
this.port = options.port !== undefined ? options.port : (parseInt(process.env.GROOVE_PORT, 10) || DEFAULT_PORT);
|
|
95
96
|
this.host = resolveHost(options.host);
|
|
96
97
|
this.projectDir = options.projectDir || process.cwd();
|
|
97
|
-
this.grooveDir = options.grooveDir || resolve(
|
|
98
|
+
this.grooveDir = options.grooveDir || resolve(homedir(), '.groove');
|
|
98
99
|
this.pidFile = resolve(this.grooveDir, 'daemon.pid');
|
|
99
100
|
|
|
100
101
|
// Ensure .groove directories exist
|
|
@@ -16,10 +16,10 @@ export class Introducer {
|
|
|
16
16
|
generateContext(newAgent, options = {}) {
|
|
17
17
|
const { taskNegotiation, hasTask, isRotation } = options;
|
|
18
18
|
const agents = this.daemon.registry.getAll();
|
|
19
|
-
// Only include ACTIVE agents
|
|
20
|
-
// Completed agents' work is captured in the journalist's project map, not here
|
|
19
|
+
// Only include ACTIVE agents from the SAME TEAM — never leak cross-team state
|
|
21
20
|
const others = agents.filter((a) => a.id !== newAgent.id &&
|
|
22
|
-
(a.status === 'running' || a.status === 'starting')
|
|
21
|
+
(a.status === 'running' || a.status === 'starting') &&
|
|
22
|
+
a.teamId === newAgent.teamId);
|
|
23
23
|
|
|
24
24
|
const lines = [
|
|
25
25
|
`# GROOVE Agent Context`,
|
|
@@ -475,7 +475,6 @@ export class Introducer {
|
|
|
475
475
|
const agents = this.daemon.registry.getAll();
|
|
476
476
|
|
|
477
477
|
if (agents.length === 0) {
|
|
478
|
-
// Clean up if no agents
|
|
479
478
|
const regPath = resolve(projectDir, 'AGENTS_REGISTRY.md');
|
|
480
479
|
if (existsSync(regPath)) {
|
|
481
480
|
writeFileSync(regPath, '');
|
|
@@ -483,25 +482,39 @@ export class Introducer {
|
|
|
483
482
|
return;
|
|
484
483
|
}
|
|
485
484
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
``,
|
|
489
|
-
`*Auto-generated by GROOVE. Do not edit manually.*`,
|
|
490
|
-
``,
|
|
491
|
-
`| ID | Name | Role | Provider | Directory | Scope | Status |`,
|
|
492
|
-
`|----|------|------|----------|-----------|-------|--------|`,
|
|
493
|
-
];
|
|
494
|
-
|
|
485
|
+
// Group agents by team so each team's registry only shows its own agents
|
|
486
|
+
const teamGroups = new Map();
|
|
495
487
|
for (const a of agents) {
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
488
|
+
const tid = a.teamId || '_default';
|
|
489
|
+
if (!teamGroups.has(tid)) teamGroups.set(tid, []);
|
|
490
|
+
teamGroups.get(tid).push(a);
|
|
499
491
|
}
|
|
500
492
|
|
|
501
|
-
|
|
502
|
-
|
|
493
|
+
// Write a scoped registry into each team's workingDir
|
|
494
|
+
for (const [teamId, teamAgents] of teamGroups) {
|
|
495
|
+
const team = teamId !== '_default' ? this.daemon.teams?.get(teamId) : null;
|
|
496
|
+
const dir = team?.workingDir || projectDir;
|
|
497
|
+
|
|
498
|
+
const lines = [
|
|
499
|
+
`# AGENTS REGISTRY`,
|
|
500
|
+
``,
|
|
501
|
+
`*Auto-generated by GROOVE. Do not edit manually.*`,
|
|
502
|
+
``,
|
|
503
|
+
`| ID | Name | Role | Provider | Directory | Scope | Status |`,
|
|
504
|
+
`|----|------|------|----------|-----------|-------|--------|`,
|
|
505
|
+
];
|
|
506
|
+
|
|
507
|
+
for (const a of teamAgents) {
|
|
508
|
+
const scope = a.scope?.length > 0 ? `\`${a.scope.join('`, `')}\`` : '-';
|
|
509
|
+
const agentDir = a.workingDir ? escapeMd(a.workingDir) : '-';
|
|
510
|
+
lines.push(`| ${escapeMd(a.id)} | ${escapeMd(a.name)} | ${escapeMd(a.role)} | ${escapeMd(a.provider)} | ${agentDir} | ${scope} | ${escapeMd(a.status)} |`);
|
|
511
|
+
}
|
|
503
512
|
|
|
504
|
-
|
|
513
|
+
lines.push('');
|
|
514
|
+
lines.push(`*Updated: ${new Date().toISOString()}*`);
|
|
515
|
+
|
|
516
|
+
writeFileSync(resolve(dir, 'AGENTS_REGISTRY.md'), lines.join('\n'));
|
|
517
|
+
}
|
|
505
518
|
}
|
|
506
519
|
|
|
507
520
|
injectGrooveSection(projectDir) {
|
|
@@ -510,7 +523,19 @@ export class Introducer {
|
|
|
510
523
|
// clobbering the user's content.
|
|
511
524
|
const claudeMdPath = resolve(projectDir, 'CLAUDE.md');
|
|
512
525
|
const agents = this.daemon.registry.getAll();
|
|
513
|
-
|
|
526
|
+
|
|
527
|
+
// Only show agents that belong to this project directory — exclude agents
|
|
528
|
+
// from teams with their own isolated workingDir (sandbox teams)
|
|
529
|
+
const isolatedDirs = new Set();
|
|
530
|
+
if (this.daemon.teams) {
|
|
531
|
+
for (const team of this.daemon.teams.list()) {
|
|
532
|
+
if (team.workingDir && team.workingDir !== projectDir) {
|
|
533
|
+
isolatedDirs.add(team.workingDir);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const running = agents.filter((a) => a.status === 'running' &&
|
|
538
|
+
!isolatedDirs.has(a.workingDir));
|
|
514
539
|
|
|
515
540
|
const grooveContent = [
|
|
516
541
|
GROOVE_SECTION_START,
|
|
@@ -697,6 +697,28 @@ export class ProcessManager {
|
|
|
697
697
|
}
|
|
698
698
|
}
|
|
699
699
|
|
|
700
|
+
// Validate provider is both installed and authenticated — fall back if not
|
|
701
|
+
const isProviderAuthed = (name) => {
|
|
702
|
+
const p = getProvider(name);
|
|
703
|
+
if (!p || !p.constructor.isInstalled()) return false;
|
|
704
|
+
const authType = p.constructor.authType;
|
|
705
|
+
if (authType === 'local' || authType === 'none') return true;
|
|
706
|
+
if (authType === 'api-key') return !!this.daemon.credentials?.hasKey(name);
|
|
707
|
+
if (authType === 'subscription') {
|
|
708
|
+
const status = p.constructor.isAuthenticated?.();
|
|
709
|
+
return status?.authenticated === true;
|
|
710
|
+
}
|
|
711
|
+
return true;
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
if (!isProviderAuthed(providerName)) {
|
|
715
|
+
const priority = ['claude-code', 'gemini', 'codex', 'local', 'ollama'];
|
|
716
|
+
const fallback = priority.find(p => p !== providerName && isProviderAuthed(p));
|
|
717
|
+
if (fallback) {
|
|
718
|
+
providerName = fallback;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
700
722
|
const provider = getProvider(providerName);
|
|
701
723
|
if (!provider) {
|
|
702
724
|
throw new Error(`Unknown provider: ${providerName}`);
|
|
@@ -717,7 +739,11 @@ export class ProcessManager {
|
|
|
717
739
|
if (config.model && config.model !== 'auto' && provider.constructor.models) {
|
|
718
740
|
const valid = provider.constructor.models.some(m => m.id === config.model);
|
|
719
741
|
if (!valid) {
|
|
720
|
-
|
|
742
|
+
const fallback = provider.constructor.models[0];
|
|
743
|
+
if (fallback) {
|
|
744
|
+
console.log(`[Groove] Model '${config.model}' not available for ${provider.constructor.displayName}, falling back to '${fallback.id}'`);
|
|
745
|
+
config.model = fallback.id;
|
|
746
|
+
}
|
|
721
747
|
}
|
|
722
748
|
}
|
|
723
749
|
|
|
@@ -1099,6 +1125,26 @@ For normal file edits within your scope, proceed without review.
|
|
|
1099
1125
|
}
|
|
1100
1126
|
}
|
|
1101
1127
|
|
|
1128
|
+
// Best-effort Codex auth recovery: refresh ~/.codex/auth.json if stale
|
|
1129
|
+
const agentProviderName = agent.provider || config.provider;
|
|
1130
|
+
if (agentProviderName === 'codex') {
|
|
1131
|
+
const codexMeta = getProvider('codex');
|
|
1132
|
+
if (codexMeta?.constructor?.isAuthenticated) {
|
|
1133
|
+
const authStatus = codexMeta.constructor.isAuthenticated();
|
|
1134
|
+
if (!authStatus?.authenticated && this.daemon.credentials?.hasKey('codex')) {
|
|
1135
|
+
const storedKey = this.daemon.credentials.getKey('codex');
|
|
1136
|
+
if (storedKey && codexMeta.constructor.onKeySet) {
|
|
1137
|
+
try {
|
|
1138
|
+
const result = await codexMeta.constructor.onKeySet(storedKey);
|
|
1139
|
+
logStream.write(`[${new Date().toISOString()}] Codex auth recovery: ${result?.ok ? 'success' : result?.error || 'failed'}\n`);
|
|
1140
|
+
} catch (e) {
|
|
1141
|
+
logStream.write(`[${new Date().toISOString()}] Codex auth recovery error: ${e.message}\n`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1102
1148
|
// Spawn the process (use pipe for stdin if provider needs to send prompt via stdin)
|
|
1103
1149
|
const spawnCwd = [providerCwd, agent.workingDir, this.daemon.projectDir].find(d => d && existsSync(d)) || this.daemon.projectDir;
|
|
1104
1150
|
const proc = cpSpawn(command, args, {
|
|
@@ -358,6 +358,39 @@ export class Teams {
|
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
promote(id) {
|
|
362
|
+
const team = this.teams.get(id);
|
|
363
|
+
if (!team) throw new Error('Team not found');
|
|
364
|
+
if (team.mode === 'production') throw new Error('Team is already in production mode');
|
|
365
|
+
|
|
366
|
+
const oldDir = team.workingDir;
|
|
367
|
+
const targetDir = this.daemon.projectDir;
|
|
368
|
+
|
|
369
|
+
const entries = readdirSync(oldDir, { withFileTypes: true });
|
|
370
|
+
for (const entry of entries) {
|
|
371
|
+
if (entry.name === '.groove') continue;
|
|
372
|
+
const src = resolve(oldDir, entry.name);
|
|
373
|
+
const dest = resolve(targetDir, entry.name);
|
|
374
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
rmSync(oldDir, { recursive: true, force: true });
|
|
378
|
+
|
|
379
|
+
team.workingDir = targetDir;
|
|
380
|
+
team.mode = 'production';
|
|
381
|
+
|
|
382
|
+
const agents = this.daemon.registry.getAll().filter((a) => a.teamId === id);
|
|
383
|
+
for (const agent of agents) {
|
|
384
|
+
if (agent.workingDir === oldDir) {
|
|
385
|
+
this.daemon.registry.update(agent.id, { workingDir: targetDir });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this._save();
|
|
390
|
+
this.daemon.broadcast({ type: 'team:updated', team });
|
|
391
|
+
return team;
|
|
392
|
+
}
|
|
393
|
+
|
|
361
394
|
// Backward compat stubs
|
|
362
395
|
onAgentChange() {}
|
|
363
396
|
getActiveTeam() { return this.getDefault()?.name || null; }
|