groove-dev 0.27.7 → 0.27.11

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 (127) hide show
  1. package/CLAUDE.md +0 -7
  2. package/node_modules/@groove-dev/daemon/src/api.js +496 -44
  3. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +25 -12
  4. package/node_modules/@groove-dev/daemon/src/index.js +7 -0
  5. package/node_modules/@groove-dev/daemon/src/introducer.js +72 -4
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +66 -11
  7. package/node_modules/@groove-dev/daemon/src/process.js +128 -104
  8. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  9. package/node_modules/@groove-dev/daemon/src/repo-import.js +541 -0
  10. package/node_modules/@groove-dev/daemon/src/rotator.js +28 -1
  11. package/node_modules/@groove-dev/daemon/src/supervisor.js +2 -1
  12. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +504 -0
  13. package/node_modules/@groove-dev/daemon/src/validate.js +13 -0
  14. package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -4
  15. package/node_modules/@groove-dev/daemon/test/rotator.test.js +4 -1
  16. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +1 -0
  17. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +677 -0
  18. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  19. package/node_modules/@groove-dev/gui/src/app.css +14 -0
  20. package/node_modules/@groove-dev/gui/src/app.jsx +13 -0
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +130 -1
  22. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  23. package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  24. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +16 -17
  25. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +141 -1
  26. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  27. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +8 -8
  28. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  29. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  30. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +4 -4
  31. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +7 -1
  32. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  33. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +14 -4
  34. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +46 -11
  35. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-card.jsx +64 -0
  36. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +363 -0
  37. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  38. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +22 -0
  39. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +48 -0
  40. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +129 -0
  41. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +243 -0
  42. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +192 -0
  43. package/node_modules/@groove-dev/gui/src/components/ui/approval-modal.jsx +63 -0
  44. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
  45. package/node_modules/@groove-dev/gui/src/lib/edition.js +4 -0
  46. package/node_modules/@groove-dev/gui/src/lib/electron.js +17 -0
  47. package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
  48. package/node_modules/@groove-dev/gui/src/stores/groove.js +150 -6
  49. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +39 -40
  50. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +82 -0
  51. package/node_modules/@groove-dev/gui/src/views/settings.jsx +66 -0
  52. package/node_modules/@groove-dev/gui/vite.config.js +3 -0
  53. package/package.json +7 -2
  54. package/packages/daemon/src/api.js +496 -44
  55. package/packages/daemon/src/gateways/manager.js +25 -12
  56. package/packages/daemon/src/index.js +7 -0
  57. package/packages/daemon/src/introducer.js +72 -4
  58. package/packages/daemon/src/journalist.js +66 -11
  59. package/packages/daemon/src/process.js +128 -104
  60. package/packages/daemon/src/registry.js +1 -1
  61. package/packages/daemon/src/repo-import.js +541 -0
  62. package/packages/daemon/src/rotator.js +28 -1
  63. package/packages/daemon/src/supervisor.js +2 -1
  64. package/packages/daemon/src/tunnel-manager.js +504 -0
  65. package/packages/daemon/src/validate.js +13 -0
  66. package/packages/gui/dist/assets/index-BE6lYcd7.css +1 -0
  67. package/packages/gui/dist/assets/index-zdzOLAZM.js +677 -0
  68. package/packages/gui/dist/index.html +2 -2
  69. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +3 -3
  70. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +2 -2
  71. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +3 -3
  72. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +5 -5
  73. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-dialog.js +3 -3
  74. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-scroll-area.js +1 -1
  75. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tabs.js +5 -5
  76. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tooltip.js +3 -3
  77. package/packages/gui/node_modules/.vite/deps/_metadata.json +53 -53
  78. package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js → chunk-DH7AESXW.js} +2 -2
  79. package/packages/gui/node_modules/.vite/deps/{chunk-KXLIKZFX.js → chunk-GFE3G4IN.js} +133 -133
  80. package/packages/gui/node_modules/.vite/deps/chunk-GFE3G4IN.js.map +7 -0
  81. package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js → chunk-LKZVMLRH.js} +6 -6
  82. package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js → chunk-MCVDVNE5.js} +2 -2
  83. package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js → chunk-SPKVQGZX.js} +6 -6
  84. package/packages/gui/src/app.css +14 -0
  85. package/packages/gui/src/app.jsx +13 -0
  86. package/packages/gui/src/components/agents/agent-config.jsx +130 -1
  87. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  88. package/packages/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  89. package/packages/gui/src/components/agents/agent-node.jsx +16 -17
  90. package/packages/gui/src/components/agents/spawn-wizard.jsx +141 -1
  91. package/packages/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  92. package/packages/gui/src/components/dashboard/intel-panel.jsx +8 -8
  93. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  94. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  95. package/packages/gui/src/components/layout/activity-bar.jsx +4 -4
  96. package/packages/gui/src/components/layout/app-shell.jsx +7 -1
  97. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  98. package/packages/gui/src/components/layout/command-palette.jsx +14 -4
  99. package/packages/gui/src/components/layout/status-bar.jsx +46 -11
  100. package/packages/gui/src/components/marketplace/repo-card.jsx +64 -0
  101. package/packages/gui/src/components/marketplace/repo-import.jsx +363 -0
  102. package/packages/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  103. package/packages/gui/src/components/pro/pro-gate.jsx +22 -0
  104. package/packages/gui/src/components/pro/upgrade-card.jsx +48 -0
  105. package/packages/gui/src/components/settings/quick-connect.jsx +129 -0
  106. package/packages/gui/src/components/settings/remote-server-card.jsx +243 -0
  107. package/packages/gui/src/components/settings/server-dialog.jsx +192 -0
  108. package/packages/gui/src/components/ui/approval-modal.jsx +63 -0
  109. package/packages/gui/src/components/ui/toast.jsx +1 -1
  110. package/packages/gui/src/lib/edition.js +4 -0
  111. package/packages/gui/src/lib/electron.js +17 -0
  112. package/packages/gui/src/lib/status.js +1 -0
  113. package/packages/gui/src/stores/groove.js +150 -6
  114. package/packages/gui/src/views/dashboard.jsx +39 -40
  115. package/packages/gui/src/views/marketplace.jsx +82 -0
  116. package/packages/gui/src/views/settings.jsx +66 -0
  117. package/packages/gui/vite.config.js +3 -0
  118. package/node_modules/@groove-dev/gui/dist/assets/index-Bl1_J0sN.js +0 -652
  119. package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +0 -1
  120. package/packages/gui/dist/assets/index-Bl1_J0sN.js +0 -652
  121. package/packages/gui/dist/assets/index-DjORRpF0.css +0 -1
  122. package/packages/gui/node_modules/.vite/deps/chunk-KXLIKZFX.js.map +0 -7
  123. package/test-slack.mjs +0 -28
  124. /package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js.map → chunk-DH7AESXW.js.map} +0 -0
  125. /package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js.map → chunk-LKZVMLRH.js.map} +0 -0
  126. /package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js.map → chunk-MCVDVNE5.js.map} +0 -0
  127. /package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js.map → chunk-SPKVQGZX.js.map} +0 -0
@@ -816,18 +816,31 @@ export class GatewayManager {
816
816
 
817
817
  // Register phase 2 for auto-spawn
818
818
  if (phase2.length > 0 && phase1Ids.length > 0) {
819
- this.daemon._pendingPhase2 = this.daemon._pendingPhase2 || [];
820
- this.daemon._pendingPhase2.push({
821
- waitFor: phase1Ids,
822
- agents: phase2.map((c) => ({
823
- role: c.role, scope: c.scope || [], prompt: c.prompt || '',
824
- provider: c.provider || 'claude-code', model: c.model || 'auto',
825
- permission: c.permission || 'auto',
826
- workingDir: c.workingDir || defaultDir,
827
- name: c.name || undefined,
828
- teamId: defaultTeamId,
829
- })),
830
- });
819
+ // Dedup: if a running idle fullstack already exists in this team,
820
+ // skip the phase2 queue — _triggerIdleQC will notify it when phase 1 completes
821
+ const teamAgents = this.daemon.registry.getAll().filter((a) => a.teamId === defaultTeamId);
822
+ const existingQC = teamAgents.find((a) =>
823
+ a.role === 'fullstack' &&
824
+ (a.status === 'running' || a.status === 'starting')
825
+ );
826
+ const qcIsIdle = existingQC && (this.daemon.journalist?.getAgentFiles(existingQC) || []).length === 0;
827
+
828
+ if (existingQC && qcIsIdle) {
829
+ this.daemon.audit.log('phase2.skipQueue', { existingQC: existingQC.id, name: existingQC.name, reason: 'idle fullstack exists', source: 'gateway' });
830
+ } else {
831
+ this.daemon._pendingPhase2 = this.daemon._pendingPhase2 || [];
832
+ this.daemon._pendingPhase2.push({
833
+ waitFor: phase1Ids,
834
+ agents: phase2.map((c) => ({
835
+ role: c.role, scope: c.scope || [], prompt: c.prompt || '',
836
+ provider: c.provider || 'claude-code', model: c.model || 'auto',
837
+ permission: c.permission || 'auto',
838
+ workingDir: c.workingDir || defaultDir,
839
+ name: c.name || undefined,
840
+ teamId: defaultTeamId,
841
+ })),
842
+ });
843
+ }
831
844
  }
832
845
 
833
846
  this.daemon.audit.log('team.launch', {
@@ -36,8 +36,10 @@ import { MemoryStore } from './memory.js';
36
36
  import { TerminalManager } from './terminal-pty.js';
37
37
  import { GatewayManager } from './gateways/manager.js';
38
38
  import { McpManager } from './mcp-manager.js';
39
+ import { TunnelManager } from './tunnel-manager.js';
39
40
  import { ModelManager } from './model-manager.js';
40
41
  import { LlamaServerManager } from './llama-server.js';
42
+ import { RepoImporter } from './repo-import.js';
41
43
  import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
42
44
 
43
45
  const DEFAULT_PORT = 31415;
@@ -137,6 +139,8 @@ export class Daemon {
137
139
  this.modelManager = new ModelManager(this);
138
140
  this.llamaServer = new LlamaServerManager(this);
139
141
  this.mcpManager = new McpManager(this);
142
+ this.tunnelManager = new TunnelManager(this);
143
+ this.repoImporter = new RepoImporter(this);
140
144
 
141
145
  // HTTP + WebSocket server
142
146
  this.app = express();
@@ -450,6 +454,9 @@ export class Daemon {
450
454
  this.fileWatcher.unwatchAll();
451
455
  this.terminalManager.killAll();
452
456
 
457
+ // Disconnect all SSH tunnels
458
+ this.tunnelManager.shutdown();
459
+
453
460
  // Kill all agent processes, stop MCP servers, and stop inference servers
454
461
  await this.processes.killAll();
455
462
  this.mcpManager.stopAll();
@@ -37,6 +37,17 @@ export class Introducer {
37
37
  lines.push(`You have no file scope restrictions.`);
38
38
  }
39
39
 
40
+ // Sandbox boundary for imported repos
41
+ if (newAgent.workingDir) {
42
+ const sandboxPath = resolve(newAgent.workingDir, '.groove', 'sandbox.json');
43
+ if (existsSync(sandboxPath)) {
44
+ lines.push('');
45
+ lines.push(`## HARD BOUNDARY`);
46
+ lines.push('');
47
+ lines.push(`You MUST NOT read, write, or modify ANY file outside \`${newAgent.workingDir}/\`. This is a sandboxed imported repo. If setup instructions require changes outside this directory, ask the user first.`);
48
+ }
49
+ }
50
+
40
51
  lines.push('');
41
52
 
42
53
  if (others.length === 0) {
@@ -249,7 +260,13 @@ export class Introducer {
249
260
  lines.push('');
250
261
  lines.push(`## Integrations (${integrationSections.length} connected)`);
251
262
  lines.push('');
252
- lines.push('You have integrations connected via GROOVE. To use them, make HTTP POST requests:');
263
+ lines.push('These integrations are ALREADY INSTALLED, AUTHENTICATED, AND READY TO USE. You do NOT need to:');
264
+ lines.push('- Ask the user for any API keys, OAuth tokens, or credentials for these services');
265
+ lines.push('- Set up authentication or run any auth flows');
266
+ lines.push('- Direct the user to any external auth pages');
267
+ lines.push('The user has already configured everything. Just use the tools.');
268
+ lines.push('');
269
+ lines.push('To use them, make HTTP POST requests:');
253
270
  lines.push('```');
254
271
  lines.push('POST http://localhost:31415/api/integrations/{id}/exec');
255
272
  lines.push('Body: {"tool": "tool_name", "params": {...}}');
@@ -257,14 +274,65 @@ export class Introducer {
257
274
  lines.push('To discover available tools: `GET http://localhost:31415/api/integrations/{id}/tools`');
258
275
  lines.push('');
259
276
  lines.push('**Approval gates:** Some tools require human approval (e.g., sending emails, creating charges).');
260
- lines.push('If you get a `requiresApproval: true` response with an `approvalId`, tell the user the action');
261
- lines.push('needs approval in the GROOVE GUI. Do NOT retry until the user confirms it has been approved.');
262
- lines.push('To retry: include `"approvalId": "<id>"` in your next exec request body.');
277
+ lines.push('If you get a `requiresApproval: true` response, the action has been queued for user approval.');
278
+ lines.push('GROOVE will show the user an approval modal and auto-execute the action once approved.');
279
+ lines.push('Do NOT tell the user to approve anything. Do NOT retry the request yourself. Just wait — you will receive a message confirming the result once the action is approved and executed.');
263
280
  lines.push('');
264
281
  lines.push(integrationSections.join('\n\n'));
265
282
  }
266
283
  }
267
284
 
285
+ // GitHub repo import — teach agents to use the tracked import API
286
+ // Attached repos — only inject repos explicitly attached to this agent
287
+ if (newAgent.repos && newAgent.repos.length > 0 && this.daemon.repoImporter) {
288
+ const repoSections = [];
289
+ for (const importId of newAgent.repos) {
290
+ const manifest = this.daemon.repoImporter.getImport(importId);
291
+ if (manifest && manifest.status === 'active') {
292
+ const stack = manifest.stackInfo ? ` (${manifest.stackInfo.runtime || 'unknown'})` : '';
293
+ repoSections.push(`- **${manifest.name || manifest.repo}**${stack}: \`${manifest.clonedTo}\` — import ID: ${manifest.id}`);
294
+ }
295
+ }
296
+ if (repoSections.length > 0) {
297
+ lines.push('');
298
+ lines.push(`## Attached Repositories (${repoSections.length})`);
299
+ lines.push('');
300
+ lines.push('These repos are cloned and attached to you. Use the paths below — do NOT re-clone them:');
301
+ lines.push(...repoSections);
302
+ lines.push('');
303
+ lines.push('If you spawn processes or modify config files for these repos, register them:');
304
+ lines.push('- `POST http://localhost:31415/api/repos/{importId}/process` with `{ "pid": <number>, "command": "description" }`');
305
+ }
306
+ }
307
+
308
+ // Lightweight import API reference for cloning new repos
309
+ lines.push('');
310
+ lines.push('## GitHub Repo Import');
311
+ lines.push('');
312
+ lines.push('To clone a NEW GitHub repo, use: `POST http://localhost:31415/api/repos/import` with `{ "repoUrl": "...", "targetPath": "~/Projects/name", "createTeam": true }`. Do NOT run `git clone` directly.');
313
+
314
+ // Surface stored API keys so agents know what's available in their environment
315
+ const KEY_MAP = { codex: 'OPENAI_API_KEY', gemini: 'GEMINI_API_KEY', ollama: 'OLLAMA_API_KEY' };
316
+ try {
317
+ const credProviders = this.daemon.credentials?.listProviders() || [];
318
+ if (credProviders.length > 0) {
319
+ lines.push('');
320
+ lines.push('## Available API Keys');
321
+ lines.push('');
322
+ lines.push('GROOVE has API keys stored and injected into your environment. Do NOT ask the user for these:');
323
+ for (const cp of credProviders) {
324
+ const envVar = KEY_MAP[cp.provider];
325
+ if (envVar) {
326
+ lines.push(`- **${cp.provider}**: available as \`${envVar}\` in your environment`);
327
+ } else {
328
+ lines.push(`- **${cp.provider}**: stored in GROOVE credentials`);
329
+ }
330
+ }
331
+ lines.push('');
332
+ lines.push('If a third-party tool needs one of these keys, it is already in your environment — do not ask the user to provide it.');
333
+ }
334
+ } catch { /* credentials not available */ }
335
+
268
336
  return lines.join('\n');
269
337
  }
270
338
 
@@ -138,7 +138,7 @@ export class Journalist {
138
138
  for (const agent of agents) {
139
139
  const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.name}.log`);
140
140
  if (!existsSync(logPath)) {
141
- result[agent.id] = { agent, entries: [] };
141
+ result[agent.id] = { agent, entries: [], explorationEntries: [] };
142
142
  continue;
143
143
  }
144
144
 
@@ -147,10 +147,10 @@ export class Journalist {
147
147
  const size = Buffer.byteLength(content);
148
148
  this.lastLogSizes[agent.id] = size;
149
149
 
150
- const entries = this.filterLog(content, agent);
151
- result[agent.id] = { agent, entries };
150
+ const { entries, explorationEntries } = this.filterLog(content, agent);
151
+ result[agent.id] = { agent, entries, explorationEntries };
152
152
  } catch {
153
- result[agent.id] = { agent, entries: [] };
153
+ result[agent.id] = { agent, entries: [], explorationEntries: [] };
154
154
  }
155
155
  }
156
156
 
@@ -160,7 +160,9 @@ export class Journalist {
160
160
  filterLog(rawLog, agent) {
161
161
  // Parse stream-json lines and extract meaningful events.
162
162
  // Focus on PROGRESS (writes, edits, commands with results) not EXPLORATION (reads, greps).
163
+ // Exploration tools are tracked separately so handoff briefs can include what was examined.
163
164
  const entries = [];
165
+ const explorationEntries = [];
164
166
  const lines = rawLog.split('\n');
165
167
  const toolResults = new Map(); // tool_use_id -> result text
166
168
 
@@ -198,8 +200,17 @@ export class Journalist {
198
200
  if (block.type === 'tool_use') {
199
201
  const tool = block.name;
200
202
 
201
- // Skip exploration tools — reads/searches are noise for synthesis
202
- if (tool === 'Read' || tool === 'Glob' || tool === 'Grep') continue;
203
+ // Track exploration tools separately they're noise for synthesis
204
+ // but valuable for handoff briefs (what files/patterns were examined)
205
+ if (tool === 'Read' || tool === 'Glob' || tool === 'Grep') {
206
+ explorationEntries.push({
207
+ type: 'exploration',
208
+ tool,
209
+ input: this.summarizeToolInput(tool, block.input),
210
+ timestamp: data.timestamp,
211
+ });
212
+ continue;
213
+ }
203
214
 
204
215
  const entry = {
205
216
  type: 'tool',
@@ -223,10 +234,11 @@ export class Journalist {
223
234
 
224
235
  entries.push(entry);
225
236
  } else if (block.type === 'text' && block.text) {
226
- // Only keep substantial reasoning (decisions, conclusions)
227
- // Short fragments like "Let me check..." are noise
237
+ // Keep reasoning that contains decisions or conclusions.
238
+ // Under 50 chars is noise ("Let me check...", "OK"), but 50-200
239
+ // often contains key decisions like "Using X instead of Y".
228
240
  const text = block.text.trim();
229
- if (text.length > 200) {
241
+ if (text.length > 50) {
230
242
  entries.push({ type: 'thinking', text: text.slice(0, 2000), timestamp: data.timestamp });
231
243
  }
232
244
  }
@@ -250,7 +262,7 @@ export class Journalist {
250
262
  }
251
263
  }
252
264
 
253
- return entries;
265
+ return { entries, explorationEntries };
254
266
  }
255
267
 
256
268
  summarizeToolInput(toolName, input) {
@@ -260,6 +272,12 @@ export class Journalist {
260
272
  return input.file_path || input.path || '';
261
273
  case 'Edit':
262
274
  return input.file_path || input.path || '';
275
+ case 'Read':
276
+ return input.file_path || input.path || '';
277
+ case 'Grep':
278
+ return `${input.pattern || ''}${input.path ? ' in ' + input.path : ''}`;
279
+ case 'Glob':
280
+ return input.pattern || '';
263
281
  case 'Bash':
264
282
  return (input.command || '').slice(0, 150);
265
283
  default:
@@ -653,10 +671,11 @@ export class Journalist {
653
671
 
654
672
  // --- Handoff Brief for Context Rotation ---
655
673
 
656
- async generateHandoffBrief(agent) {
674
+ async generateHandoffBrief(agent, options = {}) {
657
675
  const filteredLogs = this.collectFilteredLogs([agent]);
658
676
  const agentLog = filteredLogs[agent.id];
659
677
  const entries = agentLog?.entries || [];
678
+ const explorationEntries = agentLog?.explorationEntries || [];
660
679
 
661
680
  // Get current project map — scoped to agent's workspace if applicable
662
681
  const mapPath = resolve(this.daemon.projectDir, 'GROOVE_PROJECT_MAP.md');
@@ -684,6 +703,33 @@ export class Journalist {
684
703
  .slice(-3)
685
704
  .join('\n');
686
705
 
706
+ // Build exploration summary — what files/patterns were examined
707
+ const explorationSummary = explorationEntries
708
+ .map((e) => `- ${e.tool}: ${e.input}`)
709
+ .slice(-20)
710
+ .join('\n');
711
+
712
+ // Build file changes section — group Edit/Write operations by file path
713
+ const fileChanges = {};
714
+ for (const e of entries.filter((e) => e.type === 'tool' && (e.tool === 'Edit' || e.tool === 'Write'))) {
715
+ const file = e.input || 'unknown';
716
+ if (!fileChanges[file]) fileChanges[file] = [];
717
+ if (e.diff) fileChanges[file].push(e.diff);
718
+ else fileChanges[file].push(e.tool === 'Write' ? 'created' : 'modified');
719
+ }
720
+ const fileChangesSummary = Object.entries(fileChanges)
721
+ .map(([file, changes]) => `- **${file}**: ${changes.slice(0, 3).join('; ')}`)
722
+ .slice(0, 20)
723
+ .join('\n');
724
+
725
+ // Layer 7 memory: discoveries, constraints, specializations
726
+ const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 10, 2000) || '';
727
+ const constraints = this.daemon.memory?.getConstraintsMarkdown(2000) || '';
728
+ const specialization = this.daemon.memory?.getSpecialization(agent.id);
729
+ const specLine = specialization?.avgQualityScore != null
730
+ ? `- Quality profile: ${specialization.avgQualityScore}/100 across ${specialization.sessionCount} sessions`
731
+ : '';
732
+
687
733
  // Pull recent rotation history from persistent memory (Layer 7).
688
734
  // Gives the new agent causal continuity: what the last 3 agents struggled
689
735
  // with, decided, and solved — not just what the current session did.
@@ -700,11 +746,15 @@ export class Journalist {
700
746
  ? agentFeedback.map((fb) => `- "${fb.message}"`).join('\n')
701
747
  : '';
702
748
 
749
+ // Rotation reason for the new agent
750
+ const rotationTrigger = `- Rotation trigger: ${options.reason || 'manual'}${options.qualityScore ? ` (quality: ${options.qualityScore}/100)` : ''}`;
751
+
703
752
  return [
704
753
  `# Session Continuation`,
705
754
  ``,
706
755
  `You are **${agent.name}** (role: ${agent.role}). This is an internal context refresh — `,
707
756
  `the conversation with the user is ongoing and must feel seamless to them. They cannot see this brief.`,
757
+ rotationTrigger,
708
758
  ``,
709
759
  `## CRITICAL: Finish What You Were Doing`,
710
760
  ``,
@@ -729,6 +779,7 @@ export class Journalist {
729
779
  `- Scope: ${agent.scope?.join(', ') || 'unrestricted'}`,
730
780
  `- Provider: ${agent.provider}`,
731
781
  agent.workingDir ? `- Working directory: ${agent.workingDir}` : '',
782
+ specLine,
732
783
  ``,
733
784
  `## Session State`,
734
785
  `- Tokens used before refresh: ${agent.tokensUsed}`,
@@ -737,6 +788,10 @@ export class Journalist {
737
788
  toolSummary ? `### Recent tool calls (what you were doing)\n${toolSummary}\n` : '',
738
789
  resultSummary ? `### Last results\n${resultSummary}\n` : '',
739
790
  errorSummary ? `### Unresolved errors\n${errorSummary}\n` : '',
791
+ fileChangesSummary ? `## Files Modified\n\n${fileChangesSummary}\n` : '',
792
+ explorationSummary ? `## Exploration Context\n\n${explorationSummary}\n` : '',
793
+ discoveries ? `## Known Issues & Fixes\n\n${discoveries}\n` : '',
794
+ constraints ? `## Project Constraints\n\n${constraints}\n` : '',
740
795
  `## Current Project State`,
741
796
  ``,
742
797
  projectMap ? projectMap.slice(0, 10000) : 'No project map available yet.',
@@ -2,7 +2,7 @@
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
4
  import { spawn as cpSpawn } from 'child_process';
5
- import { createWriteStream, mkdirSync, chmodSync, existsSync, readFileSync, unlinkSync, readdirSync, copyFileSync } from 'fs';
5
+ import { createWriteStream, mkdirSync, chmodSync, existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync, copyFileSync } from 'fs';
6
6
  import { resolve, dirname } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { getProvider, getInstalledProviders } from './providers/index.js';
@@ -66,6 +66,19 @@ Do NOT write code unless explicitly asked. Use your MCP tools (database queries,
66
66
  - Researching topics to produce accurate, substantive writing
67
67
  You CAN use code tools to create and edit text files, markdown documents, and structured content. For best results, apply a writing skill from the Marketplace that matches your task.
68
68
 
69
+ `,
70
+ chat: `You are a Chat agent — a conversational companion, friend, and assistant. You are warm, curious, and genuinely engaged. You can discuss anything: ideas, philosophy, science, culture, coding, life.
71
+
72
+ Your primary mode is conversation, but you can still perform ANY task the user asks — code, research, analysis, writing. When no task is given, lean into being a great conversational partner.
73
+
74
+ Key behaviors:
75
+ - Be genuinely interested in the user's thoughts
76
+ - Ask thoughtful follow-up questions
77
+ - Share perspectives and explore ideas together
78
+ - Match the user's energy — playful, serious, philosophical, technical
79
+ - Remember context within the conversation
80
+ - Be honest and direct, not sycophantic
81
+
69
82
  `,
70
83
  frontend: `You are a Frontend agent. You build and modify UI components, views, and state management. Focus on:
71
84
  - Writing clean React/JSX with Tailwind CSS classes — zero inline styles unless dynamic
@@ -351,6 +364,23 @@ export class ProcessManager {
351
364
  }
352
365
  }
353
366
 
367
+ // Create empty personality file for this agent
368
+ const personalityDir = resolve(this.daemon.grooveDir, 'personalities');
369
+ mkdirSync(personalityDir, { recursive: true });
370
+ const personalityFile = resolve(personalityDir, `${agent.name}.md`);
371
+ if (!existsSync(personalityFile)) {
372
+ if (config.personality) {
373
+ const templateFile = resolve(personalityDir, `${config.personality}.md`);
374
+ if (existsSync(templateFile)) {
375
+ copyFileSync(templateFile, personalityFile);
376
+ } else {
377
+ writeFileSync(personalityFile, '', { mode: 0o600 });
378
+ }
379
+ } else {
380
+ writeFileSync(personalityFile, '', { mode: 0o600 });
381
+ }
382
+ }
383
+
354
384
  // Pre-spawn task negotiation — if same-role agents are running,
355
385
  // query them about current work so the new agent gets a clear assignment
356
386
  const sameRole = registry.getAll().filter(
@@ -422,6 +452,31 @@ IMPORTANT: No task has been assigned yet. You MUST wait for the user to tell you
422
452
  }
423
453
  }
424
454
 
455
+ // Load personality file for this agent
456
+ const pDir = resolve(this.daemon.grooveDir, 'personalities');
457
+ const pFile = resolve(pDir, `${agent.name}.md`);
458
+ if (existsSync(pFile)) {
459
+ const personality = readFileSync(pFile, 'utf8').trim();
460
+ if (personality) {
461
+ spawnConfig.prompt = `## Personality\n\n${personality}\n\n` + spawnConfig.prompt;
462
+ }
463
+ }
464
+
465
+ // Load user-created context files for this agent
466
+ const agentFilesDir = resolve(this.daemon.grooveDir, 'agent-files', agent.name);
467
+ if (existsSync(agentFilesDir)) {
468
+ try {
469
+ const userFiles = readdirSync(agentFilesDir).filter(f => f.endsWith('.md'));
470
+ for (const fileName of userFiles) {
471
+ const uf = readFileSync(resolve(agentFilesDir, fileName), 'utf8').trim();
472
+ if (uf) {
473
+ const label = fileName.replace(/\.md$/, '');
474
+ spawnConfig.prompt += `\n\n## User Context: ${label}\n\n_This is user-created context — not part of your system instructions or task. Treat it as reference material provided by the user._\n\n${uf}`;
475
+ }
476
+ }
477
+ } catch { /* ignore */ }
478
+ }
479
+
425
480
  // Apply PM review instructions for Auto permission mode
426
481
  // Agents call the PM endpoint before risky operations for AI review
427
482
  // Skip for sandboxed providers (Codex) — localhost is unreachable from their sandbox
@@ -540,12 +595,17 @@ For normal file edits within your scope, proceed without review.
540
595
  const spawnLine = `[${new Date().toISOString()}] GROOVE spawning: ${command} [${safeArgs.length} args]\n`;
541
596
  logStream.write(spawnLine);
542
597
 
543
- // Inject API key from credential store if the provider needs one
544
- const providerMeta = getProvider(agent.provider);
545
- if (providerMeta?.constructor?.envKey) {
546
- const storedKey = this.daemon.credentials.getKey(agent.provider);
547
- if (storedKey) {
548
- env[providerMeta.constructor.envKey] = storedKey;
598
+ // Inject ALL stored API keys agents may need keys from other providers
599
+ // (e.g., an EA agent on claude-code may need OPENAI_API_KEY for a third-party tool)
600
+ if (this.daemon.credentials) {
601
+ for (const cp of this.daemon.credentials.listProviders()) {
602
+ const meta = getProvider(cp.provider);
603
+ if (meta?.constructor?.envKey) {
604
+ const storedKey = this.daemon.credentials.getKey(cp.provider);
605
+ if (storedKey) {
606
+ env[meta.constructor.envKey] = storedKey;
607
+ }
608
+ }
549
609
  }
550
610
  }
551
611
 
@@ -584,69 +644,7 @@ For normal file edits within your scope, proceed without review.
584
644
  // Capture stdout (stream-json from Claude Code)
585
645
  proc.stdout.on('data', (chunk) => {
586
646
  logStream.write(chunk);
587
-
588
- const output = provider.parseOutput(chunk.toString());
589
- if (output) {
590
- // Feed to classifier for complexity tracking (informs model routing)
591
- this.daemon.classifier.addEvent(agent.id, output);
592
-
593
- const updates = { lastActivity: new Date().toISOString() };
594
- // Capture session_id for --resume support (zero cold-start continuation)
595
- if (output.sessionId) {
596
- updates.sessionId = output.sessionId;
597
- }
598
- if (output.tokensUsed !== undefined && output.tokensUsed > 0) {
599
- const current = registry.get(agent.id);
600
- if (current) {
601
- updates.tokensUsed = current.tokensUsed + output.tokensUsed;
602
- // Feed token tracker with full breakdown for savings calculations
603
- this.daemon.tokens.record(agent.id, {
604
- tokens: output.tokensUsed,
605
- inputTokens: output.inputTokens,
606
- outputTokens: output.outputTokens,
607
- cacheReadTokens: output.cacheReadTokens,
608
- cacheCreationTokens: output.cacheCreationTokens,
609
- model: output.model,
610
- estimatedCostUsd: output.estimatedCostUsd,
611
- });
612
- // Feed router cost log for tier tracking
613
- const tier = this.daemon.classifier.classify(agent.id);
614
- this.daemon.router.recordUsage(agent.id, output.model || current.model, output.tokensUsed, tier);
615
- }
616
- }
617
- // Record session result data (cost, duration, turns)
618
- if (output.type === 'result') {
619
- this.daemon.tokens.recordResult(agent.id, {
620
- costUsd: output.cost, durationMs: output.duration, turns: output.turns,
621
- });
622
- const resultUpdates = {};
623
- if (output.cost) resultUpdates.costUsd = (registry.get(agent.id)?.costUsd || 0) + output.cost;
624
- if (output.duration) resultUpdates.durationMs = output.duration;
625
- if (output.turns) resultUpdates.turns = output.turns;
626
- if (Object.keys(resultUpdates).length > 0) registry.update(agent.id, resultUpdates);
627
- }
628
- if (output.contextUsage !== undefined) {
629
- updates.contextUsage = output.contextUsage;
630
-
631
- const peak = this.peakContextUsage.get(agent.id) || 0;
632
- if (output.contextUsage > peak) {
633
- this.peakContextUsage.set(agent.id, output.contextUsage);
634
- } else if (peak >= COMPACTION_MIN_PEAK) {
635
- const drop = peak - output.contextUsage;
636
- if (drop >= COMPACTION_DROP_THRESHOLD * peak) {
637
- this.daemon.rotator.recordNaturalCompaction(agent, peak, output.contextUsage);
638
- this.peakContextUsage.set(agent.id, output.contextUsage);
639
- }
640
- }
641
- }
642
- registry.update(agent.id, updates);
643
-
644
- this.daemon.broadcast({
645
- type: 'agent:output',
646
- agentId: agent.id,
647
- data: output,
648
- });
649
- }
647
+ this._processStdoutChunk(agent.id, provider, chunk);
650
648
  });
651
649
 
652
650
  // Capture stderr — collect for crash reporting
@@ -763,6 +761,33 @@ For normal file edits within your scope, proceed without review.
763
761
  * Shared output handler for agent loop events.
764
762
  * Feeds registry, token tracker, classifier, router, and broadcasts to GUI.
765
763
  */
764
+ // Buffer stdout across chunks and parse each complete stream-json line
765
+ // separately. Two bugs this fixes:
766
+ // 1. A JSON line can span chunk boundaries — parsing each chunk in isolation
767
+ // drops the partial line, so big assistant turns vanish.
768
+ // 2. A single assistant msg_id emits multiple JSON lines (thinking, text,
769
+ // tool_use), and parseOutput merges all events in its input into one
770
+ // output — keeping only the first activity's content. Text blocks after
771
+ // a thinking block got silently dropped, which is why long planner
772
+ // plans never reached chat.
773
+ _processStdoutChunk(agentId, provider, chunk) {
774
+ const handle = this.handles.get(agentId);
775
+ if (!handle) return;
776
+
777
+ handle.stdoutBuffer = (handle.stdoutBuffer || '') + chunk.toString();
778
+ const lastNewline = handle.stdoutBuffer.lastIndexOf('\n');
779
+ if (lastNewline === -1) return;
780
+
781
+ const complete = handle.stdoutBuffer.slice(0, lastNewline + 1);
782
+ handle.stdoutBuffer = handle.stdoutBuffer.slice(lastNewline + 1);
783
+
784
+ for (const line of complete.split('\n')) {
785
+ if (!line) continue;
786
+ const output = provider.parseOutput(line);
787
+ if (output) this._handleAgentOutput(agentId, output);
788
+ }
789
+ }
790
+
766
791
  _handleAgentOutput(agentId, output) {
767
792
  const { registry, tokens, classifier, router } = this.daemon;
768
793
  const agent = registry.get(agentId);
@@ -862,6 +887,38 @@ For normal file edits within your scope, proceed without review.
862
887
  // so QC also waits for instructions instead of auditing nothing
863
888
  for (const config of group.agents) {
864
889
  if (phase1Idle) config.prompt = '';
890
+
891
+ // Dedup: check if team already has an agent with this role
892
+ const teamId = config.teamId || this.daemon.teams.getDefault()?.id || null;
893
+ const existing = teamId ? registry.getAll().find((a) =>
894
+ a.teamId === teamId && a.role === config.role && a.id !== undefined
895
+ ) : null;
896
+
897
+ if (existing && (existing.status === 'running' || existing.status === 'starting')) {
898
+ // Agent already active — reuse it instead of spawning a duplicate
899
+ if (config.prompt) {
900
+ this.sendMessage(existing.id, config.prompt).catch((err) => {
901
+ console.error(`[Groove] Phase 2 reuse message failed for ${existing.name}: ${err.message}`);
902
+ });
903
+ }
904
+ this.daemon.audit.log('phase2.reuse', { id: existing.id, name: existing.name, role: existing.role });
905
+ this.daemon.broadcast({
906
+ type: 'phase2:spawned',
907
+ agentId: existing.id,
908
+ name: existing.name,
909
+ role: existing.role,
910
+ reused: true,
911
+ });
912
+ continue;
913
+ }
914
+
915
+ if (existing && (existing.status === 'completed' || existing.status === 'stopped' || existing.status === 'crashed' || existing.status === 'killed')) {
916
+ // Previous agent finished — remove it and respawn with the same name
917
+ config.name = existing.name;
918
+ registry.remove(existing.id);
919
+ this.daemon.locks.release(existing.id);
920
+ }
921
+
865
922
  try {
866
923
  const validated = validateAgentConfig(config);
867
924
  if (!validated.teamId) validated.teamId = this.daemon.teams.getDefault()?.id || null;
@@ -1079,40 +1136,7 @@ For normal file edits within your scope, proceed without review.
1079
1136
  // Same stdout/stderr/exit handling as spawn
1080
1137
  proc.stdout.on('data', (chunk) => {
1081
1138
  logStream.write(chunk);
1082
- const output = provider.parseOutput(chunk.toString());
1083
- if (output) {
1084
- this.daemon.classifier.addEvent(newAgent.id, output);
1085
- const updates = { lastActivity: new Date().toISOString() };
1086
- if (output.sessionId) updates.sessionId = output.sessionId;
1087
- if (output.tokensUsed !== undefined && output.tokensUsed > 0) {
1088
- const current = registry.get(newAgent.id);
1089
- if (current) {
1090
- updates.tokensUsed = current.tokensUsed + output.tokensUsed;
1091
- this.daemon.tokens.record(newAgent.id, {
1092
- tokens: output.tokensUsed,
1093
- inputTokens: output.inputTokens,
1094
- outputTokens: output.outputTokens,
1095
- cacheReadTokens: output.cacheReadTokens,
1096
- cacheCreationTokens: output.cacheCreationTokens,
1097
- model: output.model,
1098
- estimatedCostUsd: output.estimatedCostUsd,
1099
- });
1100
- }
1101
- }
1102
- if (output.type === 'result') {
1103
- this.daemon.tokens.recordResult(newAgent.id, {
1104
- costUsd: output.cost, durationMs: output.duration, turns: output.turns,
1105
- });
1106
- const resultUpdates = {};
1107
- if (output.cost) resultUpdates.costUsd = (registry.get(newAgent.id)?.costUsd || 0) + output.cost;
1108
- if (output.duration) resultUpdates.durationMs = output.duration;
1109
- if (output.turns) resultUpdates.turns = output.turns;
1110
- if (Object.keys(resultUpdates).length > 0) registry.update(newAgent.id, resultUpdates);
1111
- }
1112
- if (output.contextUsage !== undefined) updates.contextUsage = output.contextUsage;
1113
- registry.update(newAgent.id, updates);
1114
- this.daemon.broadcast({ type: 'agent:output', agentId: newAgent.id, data: output });
1115
- }
1139
+ this._processStdoutChunk(newAgent.id, provider, chunk);
1116
1140
  });
1117
1141
 
1118
1142
  proc.stderr.on('data', (chunk) => { logStream.write(`[stderr] ${chunk}`); });
@@ -51,7 +51,7 @@ export class Registry extends EventEmitter {
51
51
  if (!agent) return null;
52
52
 
53
53
  // Only allow known fields to prevent prototype pollution
54
- const SAFE_FIELDS = ['status', 'pid', 'tokensUsed', 'contextUsage', 'lastActivity', 'model', 'provider', 'name', 'routingMode', 'routingReason', 'sessionId', 'skills', 'integrations', 'workingDir', 'effort', 'costUsd', 'durationMs', 'turns', 'inputTokens', 'outputTokens', 'teamId', 'permission', 'scope', 'integrationApproval'];
54
+ const SAFE_FIELDS = ['status', 'pid', 'tokensUsed', 'contextUsage', 'lastActivity', 'model', 'provider', 'name', 'routingMode', 'routingReason', 'sessionId', 'skills', 'integrations', 'repos', 'workingDir', 'effort', 'costUsd', 'durationMs', 'turns', 'inputTokens', 'outputTokens', 'teamId', 'permission', 'scope', 'integrationApproval', 'personality'];
55
55
  for (const key of Object.keys(updates)) {
56
56
  if (SAFE_FIELDS.includes(key)) {
57
57
  agent[key] = updates[key];