groove-dev 0.27.118 → 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 +62 -0
- package/moe-training/client/transmission-queue.js +6 -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 +43 -14
- package/node_modules/@groove-dev/daemon/src/conversations.js +54 -32
- package/node_modules/@groove-dev/daemon/src/index.js +6 -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 +103 -39
- package/node_modules/@groove-dev/daemon/src/validate.js +10 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-DdN9RVnC.css → index-BLd3MON8.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-BunEIVjD.js → index-oKbzuMnX.js} +1728 -1728
- 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/app.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +3 -41
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +68 -2
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +4 -43
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +80 -3
- 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/teams/team-removal-dialog.jsx +7 -3
- package/node_modules/@groove-dev/gui/src/components/ui/data-sharing-modal.jsx +101 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +42 -5
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +47 -11
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +50 -84
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +65 -1
- package/node_modules/moe-training/client/trajectory-capture.js +62 -0
- package/node_modules/moe-training/client/transmission-queue.js +6 -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 +43 -14
- package/packages/daemon/src/conversations.js +54 -32
- package/packages/daemon/src/index.js +6 -1
- package/packages/daemon/src/introducer.js +45 -20
- package/packages/daemon/src/process.js +47 -1
- package/packages/daemon/src/teams.js +103 -39
- package/packages/daemon/src/validate.js +10 -0
- package/packages/gui/dist/assets/{index-DdN9RVnC.css → index-BLd3MON8.css} +1 -1
- package/packages/gui/dist/assets/{index-BunEIVjD.js → index-oKbzuMnX.js} +1728 -1728
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.jsx +2 -0
- package/packages/gui/src/components/agents/agent-config.jsx +3 -41
- package/packages/gui/src/components/agents/agent-file-tree.jsx +68 -2
- package/packages/gui/src/components/agents/spawn-wizard.jsx +4 -43
- package/packages/gui/src/components/editor/file-tree.jsx +80 -3
- 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/teams/team-removal-dialog.jsx +7 -3
- package/packages/gui/src/components/ui/data-sharing-modal.jsx +101 -0
- package/packages/gui/src/stores/groove.js +42 -5
- package/packages/gui/src/views/agents.jsx +47 -11
- package/packages/gui/src/views/settings.jsx +50 -84
- package/packages/gui/src/views/teams.jsx +65 -1
|
@@ -34,6 +34,12 @@ export class TrajectoryCapture {
|
|
|
34
34
|
this._offlineRetryTimer = null;
|
|
35
35
|
this._contexts = new Map();
|
|
36
36
|
this._shutdown = false;
|
|
37
|
+
this._onEnvelopeSent = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
set onEnvelopeSent(fn) {
|
|
41
|
+
this._onEnvelopeSent = typeof fn === 'function' ? fn : null;
|
|
42
|
+
if (this._transmissionQueue) this._transmissionQueue.onSent = this._onEnvelopeSent;
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
async init() {
|
|
@@ -46,6 +52,7 @@ export class TrajectoryCapture {
|
|
|
46
52
|
this._scrubber = new PIIScrubber();
|
|
47
53
|
this._attestation = new SessionAttestation(this._centralCommandUrl);
|
|
48
54
|
this._transmissionQueue = new TransmissionQueue(this._centralCommandUrl);
|
|
55
|
+
if (this._onEnvelopeSent) this._transmissionQueue.onSent = this._onEnvelopeSent;
|
|
49
56
|
this._transmissionQueue.start();
|
|
50
57
|
this._domainTagger = new DomainTagger();
|
|
51
58
|
await this._domainTagger.init();
|
|
@@ -121,6 +128,61 @@ export class TrajectoryCapture {
|
|
|
121
128
|
await this._attestation.openSession(sessionId, metadata);
|
|
122
129
|
}
|
|
123
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
|
+
|
|
124
186
|
onStdoutLine(agentId, jsonLine) {
|
|
125
187
|
if (!this._enabled) return;
|
|
126
188
|
const ctx = this._contexts.get(agentId);
|
|
@@ -10,6 +10,7 @@ export class TransmissionQueue {
|
|
|
10
10
|
this._offlineQueue = [];
|
|
11
11
|
this._running = false;
|
|
12
12
|
this._drainPromise = null;
|
|
13
|
+
this._onSent = null;
|
|
13
14
|
|
|
14
15
|
if (process.env.NODE_ENV === 'production' && !centralCommandUrl.startsWith('https://')) {
|
|
15
16
|
console.warn('[TransmissionQueue] WARNING: centralCommandUrl does not use HTTPS in production');
|
|
@@ -20,6 +21,10 @@ export class TransmissionQueue {
|
|
|
20
21
|
return this._offlineQueue.length;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
set onSent(fn) {
|
|
25
|
+
this._onSent = typeof fn === 'function' ? fn : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
enqueue(signedEnvelope) {
|
|
24
29
|
if (this._queue.length >= this._maxSize) return;
|
|
25
30
|
if (signedEnvelope?.attestation?.session_hmac === 'OFFLINE') {
|
|
@@ -86,6 +91,7 @@ export class TransmissionQueue {
|
|
|
86
91
|
});
|
|
87
92
|
if (res.ok) {
|
|
88
93
|
success = true;
|
|
94
|
+
if (this._onSent) this._onSent(envelope);
|
|
89
95
|
break;
|
|
90
96
|
}
|
|
91
97
|
} catch {
|
|
@@ -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 ────────────────────────────
|
|
@@ -16,7 +16,7 @@ import { OllamaProvider } from './providers/ollama.js';
|
|
|
16
16
|
import { ClaudeCodeProvider } from './providers/claude-code.js';
|
|
17
17
|
import { supportsSignalFlag, compareSemver, parseSemver } from './providers/groove-network.js';
|
|
18
18
|
import { ConsentManager } from '../../../moe-training/client/index.js';
|
|
19
|
-
import { validateAgentConfig, validateReasoningEffort, validateVerbosity } from './validate.js';
|
|
19
|
+
import { validateAgentConfig, validateReasoningEffort, validateVerbosity, validateTeamMode } from './validate.js';
|
|
20
20
|
import { ROLE_INTEGRATIONS, wrapWithRoleReminder } from './process.js';
|
|
21
21
|
|
|
22
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -1092,8 +1092,8 @@ export function createApi(app, daemon) {
|
|
|
1092
1092
|
|
|
1093
1093
|
app.post('/api/teams', (req, res) => {
|
|
1094
1094
|
try {
|
|
1095
|
-
const team = daemon.teams.create(req.body.name, req.body.
|
|
1096
|
-
daemon.audit.log('team.create', { id: team.id, name: team.name, workingDir: team.workingDir });
|
|
1095
|
+
const team = daemon.teams.create(req.body.name, { mode: req.body.mode });
|
|
1096
|
+
daemon.audit.log('team.create', { id: team.id, name: team.name, mode: team.mode, workingDir: team.workingDir });
|
|
1097
1097
|
res.status(201).json(team);
|
|
1098
1098
|
} catch (err) {
|
|
1099
1099
|
res.status(400).json({ error: err.message });
|
|
@@ -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
|
|
@@ -3447,9 +3458,17 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3447
3458
|
}
|
|
3448
3459
|
const defaultTeamId = launchTeamId || daemon.teams.getDefault()?.id || null;
|
|
3449
3460
|
|
|
3461
|
+
// Determine team build mode
|
|
3462
|
+
let launchMode;
|
|
3463
|
+
try { launchMode = validateTeamMode(req.body?.mode || raw.mode); } catch { launchMode = 'sandbox'; }
|
|
3464
|
+
|
|
3450
3465
|
// If planner specified a project directory, create it and use it as workingDir
|
|
3466
|
+
// Production mode: always use projectDir directly, skip subdirectory creation
|
|
3451
3467
|
let projectWorkingDir = baseDir;
|
|
3452
|
-
if (
|
|
3468
|
+
if (launchMode === 'production') {
|
|
3469
|
+
projectWorkingDir = daemon.projectDir;
|
|
3470
|
+
console.log(`[Groove] Production mode — working in project root: ${projectWorkingDir}`);
|
|
3471
|
+
} else if (projectDir) {
|
|
3453
3472
|
// Sanitize: kebab-case, no path traversal
|
|
3454
3473
|
const safeName = String(projectDir).replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64);
|
|
3455
3474
|
projectWorkingDir = resolve(baseDir, safeName);
|
|
@@ -3469,6 +3488,15 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3469
3488
|
});
|
|
3470
3489
|
}
|
|
3471
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
|
+
|
|
3472
3500
|
// Team-level overrides from the pre-planner config panel
|
|
3473
3501
|
const teamProvider = req.body?.teamProvider || undefined;
|
|
3474
3502
|
const teamModel = req.body?.teamModel || undefined;
|
|
@@ -3492,10 +3520,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3492
3520
|
|
|
3493
3521
|
// Safety net: if planner forgot the QC agent, auto-add one
|
|
3494
3522
|
if (phase2.length === 0 && phase1.length >= 2) {
|
|
3523
|
+
const { provider: qcProvider, model: qcModel } = resolveProviderAndModel(teamProvider, teamModel);
|
|
3495
3524
|
phase2 = [{
|
|
3496
3525
|
name: 'qc-agent',
|
|
3497
3526
|
role: 'fullstack', phase: 2, scope: [],
|
|
3498
|
-
provider:
|
|
3527
|
+
provider: qcProvider,
|
|
3528
|
+
model: qcModel,
|
|
3499
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.',
|
|
3500
3530
|
}];
|
|
3501
3531
|
}
|
|
@@ -3548,8 +3578,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3548
3578
|
role: existing.role,
|
|
3549
3579
|
scope: normalizeScope(config.scope || existing.scope || [], existing.workingDir || projectWorkingDir),
|
|
3550
3580
|
prompt,
|
|
3551
|
-
|
|
3552
|
-
model: config.model || existing.model || daemon.config?.defaultModel || 'auto',
|
|
3581
|
+
...resolveProviderAndModel(config.provider, config.model, existing.provider, existing.model),
|
|
3553
3582
|
permission: config.permission || existing.permission || 'auto',
|
|
3554
3583
|
workingDir: existing.workingDir || projectWorkingDir,
|
|
3555
3584
|
name: existing.name,
|
|
@@ -3573,8 +3602,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3573
3602
|
role: config.role,
|
|
3574
3603
|
scope: normalizeScope(config.scope || [], config.workingDir || projectWorkingDir),
|
|
3575
3604
|
prompt,
|
|
3576
|
-
|
|
3577
|
-
model: config.model || daemon.config?.defaultModel || 'auto',
|
|
3605
|
+
...resolveProviderAndModel(config.provider, config.model),
|
|
3578
3606
|
permission: config.permission || 'auto',
|
|
3579
3607
|
workingDir: config.workingDir || projectWorkingDir,
|
|
3580
3608
|
name: config.name || undefined,
|
|
@@ -3616,7 +3644,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3616
3644
|
waitFor: phase1Ids,
|
|
3617
3645
|
agents: phase2.map((c) => ({
|
|
3618
3646
|
role: c.role, scope: c.scope || [], prompt: c.prompt || '',
|
|
3619
|
-
|
|
3647
|
+
...resolveProviderAndModel(c.provider, c.model),
|
|
3620
3648
|
permission: c.permission || 'auto',
|
|
3621
3649
|
reasoningEffort: c.reasoningEffort, temperature: c.temperature, verbosity: c.verbosity,
|
|
3622
3650
|
workingDir: c.workingDir || projectWorkingDir,
|
|
@@ -4990,6 +5018,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4990
5018
|
'port', 'journalistInterval', 'rotationThreshold', 'autoRotation',
|
|
4991
5019
|
'qcThreshold', 'maxAgents', 'defaultProvider', 'defaultWorkingDir',
|
|
4992
5020
|
'onboardingDismissed', 'defaultModel', 'defaultChatProvider', 'defaultChatModel',
|
|
5021
|
+
'dataSharingDismissed',
|
|
4993
5022
|
];
|
|
4994
5023
|
for (const key of Object.keys(req.body)) {
|
|
4995
5024
|
if (!ALLOWED_KEYS.includes(key)) {
|
|
@@ -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
|
|
@@ -686,6 +687,10 @@ export class Daemon {
|
|
|
686
687
|
centralCommandUrl: process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai',
|
|
687
688
|
grooveVersion: version,
|
|
688
689
|
});
|
|
690
|
+
this.trajectoryCapture.onEnvelopeSent = () => {
|
|
691
|
+
const count = (this.state.get('training_envelopes_sent') || 0) + 1;
|
|
692
|
+
this.state.set('training_envelopes_sent', count);
|
|
693
|
+
};
|
|
689
694
|
this.trajectoryCapture.init();
|
|
690
695
|
} catch (e) {
|
|
691
696
|
// Training capture is never critical
|
|
@@ -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,
|