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.
Files changed (61) hide show
  1. package/moe-training/client/trajectory-capture.js +62 -0
  2. package/moe-training/client/transmission-queue.js +6 -0
  3. package/moe-training/test/client/trajectory-capture.test.js +63 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/cli/src/commands/start.js +2 -1
  6. package/node_modules/@groove-dev/daemon/package.json +1 -1
  7. package/node_modules/@groove-dev/daemon/src/api.js +43 -14
  8. package/node_modules/@groove-dev/daemon/src/conversations.js +54 -32
  9. package/node_modules/@groove-dev/daemon/src/index.js +6 -1
  10. package/node_modules/@groove-dev/daemon/src/introducer.js +45 -20
  11. package/node_modules/@groove-dev/daemon/src/process.js +47 -1
  12. package/node_modules/@groove-dev/daemon/src/teams.js +103 -39
  13. package/node_modules/@groove-dev/daemon/src/validate.js +10 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/{index-DdN9RVnC.css → index-BLd3MON8.css} +1 -1
  15. package/node_modules/@groove-dev/gui/dist/assets/{index-BunEIVjD.js → index-oKbzuMnX.js} +1728 -1728
  16. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  17. package/node_modules/@groove-dev/gui/package.json +1 -1
  18. package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +3 -41
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +68 -2
  21. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +4 -43
  22. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +80 -3
  23. package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +8 -23
  24. package/node_modules/@groove-dev/gui/src/components/settings/ProviderSetupWizard.jsx +54 -143
  25. package/node_modules/@groove-dev/gui/src/components/teams/team-removal-dialog.jsx +7 -3
  26. package/node_modules/@groove-dev/gui/src/components/ui/data-sharing-modal.jsx +101 -0
  27. package/node_modules/@groove-dev/gui/src/stores/groove.js +42 -5
  28. package/node_modules/@groove-dev/gui/src/views/agents.jsx +47 -11
  29. package/node_modules/@groove-dev/gui/src/views/settings.jsx +50 -84
  30. package/node_modules/@groove-dev/gui/src/views/teams.jsx +65 -1
  31. package/node_modules/moe-training/client/trajectory-capture.js +62 -0
  32. package/node_modules/moe-training/client/transmission-queue.js +6 -0
  33. package/node_modules/moe-training/test/client/trajectory-capture.test.js +63 -0
  34. package/package.json +1 -1
  35. package/packages/cli/package.json +1 -1
  36. package/packages/cli/src/commands/start.js +2 -1
  37. package/packages/daemon/package.json +1 -1
  38. package/packages/daemon/src/api.js +43 -14
  39. package/packages/daemon/src/conversations.js +54 -32
  40. package/packages/daemon/src/index.js +6 -1
  41. package/packages/daemon/src/introducer.js +45 -20
  42. package/packages/daemon/src/process.js +47 -1
  43. package/packages/daemon/src/teams.js +103 -39
  44. package/packages/daemon/src/validate.js +10 -0
  45. package/packages/gui/dist/assets/{index-DdN9RVnC.css → index-BLd3MON8.css} +1 -1
  46. package/packages/gui/dist/assets/{index-BunEIVjD.js → index-oKbzuMnX.js} +1728 -1728
  47. package/packages/gui/dist/index.html +2 -2
  48. package/packages/gui/package.json +1 -1
  49. package/packages/gui/src/app.jsx +2 -0
  50. package/packages/gui/src/components/agents/agent-config.jsx +3 -41
  51. package/packages/gui/src/components/agents/agent-file-tree.jsx +68 -2
  52. package/packages/gui/src/components/agents/spawn-wizard.jsx +4 -43
  53. package/packages/gui/src/components/editor/file-tree.jsx +80 -3
  54. package/packages/gui/src/components/onboarding/setup-wizard.jsx +8 -23
  55. package/packages/gui/src/components/settings/ProviderSetupWizard.jsx +54 -143
  56. package/packages/gui/src/components/teams/team-removal-dialog.jsx +7 -3
  57. package/packages/gui/src/components/ui/data-sharing-modal.jsx +101 -0
  58. package/packages/gui/src/stores/groove.js +42 -5
  59. package/packages/gui/src/views/agents.jsx +47 -11
  60. package/packages/gui/src/views/settings.jsx +50 -84
  61. 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();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.118",
3
+ "version": "0.27.120",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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(process.cwd(), '.groove');
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 ────────────────────────────
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.118",
3
+ "version": "0.27.120",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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.workingDir);
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 > 5 * 1024 * 1024) {
2961
- return res.status(400).json({ error: 'File too large (>5MB)' });
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 > 5 * 1024 * 1024) {
2994
- return res.status(400).json({ error: 'Content too large (>5MB)' });
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 (projectDir) {
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: teamProvider || plannerProvider || daemon.config?.defaultProvider || undefined,
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
- provider: config.provider || plannerProvider || daemon.config?.defaultProvider || existing.provider || undefined,
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
- provider: config.provider || plannerProvider || daemon.config?.defaultProvider || undefined,
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
- provider: c.provider || plannerProvider || daemon.config?.defaultProvider || undefined, model: c.model || daemon.config?.defaultModel || 'auto',
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
- this.daemon.broadcast({
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
- this.daemon.broadcast({
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
- this.daemon.broadcast({
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
- this.daemon.broadcast({
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
- this.daemon.broadcast({
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
- this.daemon.broadcast({
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(this.projectDir, '.groove');
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 not completed/killed ones from previous sessions
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
- const lines = [
487
- `# AGENTS REGISTRY`,
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 scope = a.scope?.length > 0 ? `\`${a.scope.join('`, `')}\`` : '-';
497
- const dir = a.workingDir ? escapeMd(a.workingDir) : '-';
498
- lines.push(`| ${escapeMd(a.id)} | ${escapeMd(a.name)} | ${escapeMd(a.role)} | ${escapeMd(a.provider)} | ${dir} | ${scope} | ${escapeMd(a.status)} |`);
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
- lines.push('');
502
- lines.push(`*Updated: ${new Date().toISOString()}*`);
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
- writeFileSync(resolve(projectDir, 'AGENTS_REGISTRY.md'), lines.join('\n'));
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
- const running = agents.filter((a) => a.status === 'running');
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,