groove-dev 0.27.26 → 0.27.28

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 (49) hide show
  1. package/.groove-staging/state.json +3 -0
  2. package/.groove-staging/timeline.json +13 -0
  3. package/CLAUDE.md +0 -10
  4. package/DECENTRALIZED_NET_WP_V1.md +871 -0
  5. package/README.md +28 -0
  6. package/SECURITY_SWEEP.md +228 -0
  7. package/decentralized-net/ACTION_PLAN.md +422 -0
  8. package/node_modules/@groove-dev/cli/package.json +1 -1
  9. package/node_modules/@groove-dev/daemon/package.json +1 -1
  10. package/node_modules/@groove-dev/daemon/src/api.js +99 -0
  11. package/node_modules/@groove-dev/daemon/src/introducer.js +7 -7
  12. package/node_modules/@groove-dev/daemon/src/journalist.js +36 -6
  13. package/node_modules/@groove-dev/daemon/src/memory.js +29 -10
  14. package/node_modules/@groove-dev/daemon/src/process.js +29 -12
  15. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +26 -1
  16. package/node_modules/@groove-dev/daemon/src/providers/codex.js +34 -11
  17. package/node_modules/@groove-dev/daemon/src/rotator.js +24 -1
  18. package/node_modules/@groove-dev/daemon/test/introducer.test.js +63 -0
  19. package/node_modules/@groove-dev/daemon/test/journalist.test.js +106 -0
  20. package/node_modules/@groove-dev/daemon/test/memory.test.js +49 -0
  21. package/node_modules/@groove-dev/daemon/test/rotator.test.js +99 -0
  22. package/node_modules/@groove-dev/gui/dist/assets/{index-DieCV-v1.js → index-Ch1N9G4Z.js} +1728 -1728
  23. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  24. package/node_modules/@groove-dev/gui/package.json +1 -1
  25. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +147 -21
  26. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +206 -44
  27. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +11 -24
  28. package/node_modules/@groove-dev/gui/src/components/marketplace/marketplace-card.jsx +1 -36
  29. package/node_modules/@groove-dev/gui/src/lib/integration-logos.js +39 -0
  30. package/package.json +1 -1
  31. package/packages/cli/package.json +1 -1
  32. package/packages/daemon/package.json +1 -1
  33. package/packages/daemon/src/api.js +99 -0
  34. package/packages/daemon/src/introducer.js +7 -7
  35. package/packages/daemon/src/journalist.js +36 -6
  36. package/packages/daemon/src/memory.js +29 -10
  37. package/packages/daemon/src/process.js +29 -12
  38. package/packages/daemon/src/providers/claude-code.js +26 -1
  39. package/packages/daemon/src/providers/codex.js +34 -11
  40. package/packages/daemon/src/rotator.js +24 -1
  41. package/packages/gui/dist/assets/{index-DieCV-v1.js → index-Ch1N9G4Z.js} +1728 -1728
  42. package/packages/gui/dist/index.html +1 -1
  43. package/packages/gui/package.json +1 -1
  44. package/packages/gui/src/components/agents/agent-config.jsx +147 -21
  45. package/packages/gui/src/components/agents/spawn-wizard.jsx +206 -44
  46. package/packages/gui/src/components/marketplace/integration-wizard.jsx +11 -24
  47. package/packages/gui/src/components/marketplace/marketplace-card.jsx +1 -36
  48. package/packages/gui/src/lib/integration-logos.js +39 -0
  49. package/MUST_FIX_ISSUES.md +0 -305
@@ -4,42 +4,7 @@ import { cn } from '../../lib/cn';
4
4
  import { Badge } from '../ui/badge';
5
5
  import { fmtNum } from '../../lib/format';
6
6
 
7
- // Well-known integration logos via CDN (simple-icons on cdn.simpleicons.org)
8
- export const INTEGRATION_LOGOS = {
9
- 'google-workspace': 'https://cdn.simpleicons.org/google/white',
10
- github: 'https://cdn.simpleicons.org/github/white',
11
- stripe: 'https://cdn.simpleicons.org/stripe/635BFF',
12
- gmail: 'https://cdn.simpleicons.org/gmail/EA4335',
13
- 'google-calendar': 'https://cdn.simpleicons.org/googlecalendar/4285F4',
14
- 'google-drive': 'https://cdn.simpleicons.org/googledrive/4285F4',
15
- 'google-docs': 'https://cdn.simpleicons.org/googledocs/4285F4',
16
- 'google-sheets': 'https://cdn.simpleicons.org/googlesheets/34A853',
17
- 'google-slides': 'https://cdn.simpleicons.org/googleslides/FBBC04',
18
- 'google-maps': 'https://cdn.simpleicons.org/googlemaps/4285F4',
19
- postgres: 'https://cdn.simpleicons.org/postgresql/4169E1',
20
- notion: 'https://cdn.simpleicons.org/notion/white',
21
- linear: 'https://cdn.simpleicons.org/linear/5E6AD2',
22
- 'brave-search': 'https://cdn.simpleicons.org/brave/FB542B',
23
- 'home-assistant': 'https://cdn.simpleicons.org/homeassistant/18BCF2',
24
- sentry: 'https://cdn.simpleicons.org/sentry/362D59',
25
- elevenlabs: 'https://cdn.simpleicons.org/elevenlabs/white',
26
- hubspot: 'https://cdn.simpleicons.org/hubspot/FF7A59',
27
- jira: 'https://cdn.simpleicons.org/jira/0052CC',
28
- sendgrid: 'https://cdn.simpleicons.org/sendgrid/1A82E2',
29
- resend: 'https://cdn.simpleicons.org/resend/white',
30
- replicate: 'https://cdn.simpleicons.org/replicate/white',
31
- vercel: 'https://cdn.simpleicons.org/vercel/white',
32
- supabase: 'https://cdn.simpleicons.org/supabase/3FCF8E',
33
- mixpanel: 'https://cdn.simpleicons.org/mixpanel/7856FF',
34
- datadog: 'https://cdn.simpleicons.org/datadog/632CA6',
35
- airtable: 'https://cdn.simpleicons.org/airtable/18BFFF',
36
- zendesk: 'https://cdn.simpleicons.org/zendesk/03363D',
37
- intercom: 'https://cdn.simpleicons.org/intercom/6AFDEF',
38
- twilio: 'https://cdn.simpleicons.org/twilio/F22F46',
39
- telnyx: 'https://cdn.simpleicons.org/telnyx/00C08B',
40
- aws: 'https://cdn.simpleicons.org/amazonaws/FF9900',
41
- plaid: 'https://cdn.simpleicons.org/plaid/white',
42
- };
7
+ import { INTEGRATION_LOGOS } from '../../lib/integration-logos';
43
8
 
44
9
  function ItemIcon({ item, size = 40 }) {
45
10
  const logoUrl = INTEGRATION_LOGOS[item.id];
@@ -0,0 +1,39 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ export const INTEGRATION_LOGOS = {
4
+ 'google-workspace': 'https://cdn.simpleicons.org/google/white',
5
+ slack: 'https://cdn.simpleicons.org/slack/E01E5A',
6
+ discord: 'https://cdn.simpleicons.org/discord/5865F2',
7
+ github: 'https://cdn.simpleicons.org/github/white',
8
+ stripe: 'https://cdn.simpleicons.org/stripe/635BFF',
9
+ gmail: 'https://cdn.simpleicons.org/gmail/EA4335',
10
+ 'google-calendar': 'https://cdn.simpleicons.org/googlecalendar/4285F4',
11
+ 'google-drive': 'https://cdn.simpleicons.org/googledrive/4285F4',
12
+ 'google-docs': 'https://cdn.simpleicons.org/googledocs/4285F4',
13
+ 'google-sheets': 'https://cdn.simpleicons.org/googlesheets/34A853',
14
+ 'google-slides': 'https://cdn.simpleicons.org/googleslides/FBBC04',
15
+ 'google-maps': 'https://cdn.simpleicons.org/googlemaps/4285F4',
16
+ postgres: 'https://cdn.simpleicons.org/postgresql/4169E1',
17
+ notion: 'https://cdn.simpleicons.org/notion/white',
18
+ linear: 'https://cdn.simpleicons.org/linear/5E6AD2',
19
+ 'brave-search': 'https://cdn.simpleicons.org/brave/FB542B',
20
+ 'home-assistant': 'https://cdn.simpleicons.org/homeassistant/18BCF2',
21
+ sentry: 'https://cdn.simpleicons.org/sentry/362D59',
22
+ elevenlabs: 'https://cdn.simpleicons.org/elevenlabs/white',
23
+ hubspot: 'https://cdn.simpleicons.org/hubspot/FF7A59',
24
+ jira: 'https://cdn.simpleicons.org/jira/0052CC',
25
+ sendgrid: 'https://cdn.simpleicons.org/sendgrid/1A82E2',
26
+ resend: 'https://cdn.simpleicons.org/resend/white',
27
+ replicate: 'https://cdn.simpleicons.org/replicate/white',
28
+ vercel: 'https://cdn.simpleicons.org/vercel/white',
29
+ supabase: 'https://cdn.simpleicons.org/supabase/3FCF8E',
30
+ mixpanel: 'https://cdn.simpleicons.org/mixpanel/7856FF',
31
+ datadog: 'https://cdn.simpleicons.org/datadog/632CA6',
32
+ airtable: 'https://cdn.simpleicons.org/airtable/18BFFF',
33
+ zendesk: 'https://cdn.simpleicons.org/zendesk/03363D',
34
+ intercom: 'https://cdn.simpleicons.org/intercom/6AFDEF',
35
+ twilio: 'https://cdn.simpleicons.org/twilio/F22F46',
36
+ telnyx: 'https://cdn.simpleicons.org/telnyx/00C08B',
37
+ aws: 'https://cdn.simpleicons.org/amazonaws/FF9900',
38
+ plaid: 'https://cdn.simpleicons.org/plaid/white',
39
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.26",
3
+ "version": "0.27.28",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.26",
3
+ "version": "0.27.28",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.26",
3
+ "version": "0.27.28",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -9,7 +9,9 @@ import { spawn, execFile } from 'child_process';
9
9
  import { lookup as mimeLookup } from './mimetypes.js';
10
10
  import { listProviders, getProvider } from './providers/index.js';
11
11
  import { OllamaProvider } from './providers/ollama.js';
12
+ import { ClaudeCodeProvider } from './providers/claude-code.js';
12
13
  import { validateAgentConfig } from './validate.js';
14
+ import { ROLE_INTEGRATIONS } from './process.js';
13
15
 
14
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
17
  const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
@@ -179,6 +181,88 @@ export function createApi(app, daemon) {
179
181
  res.json({ ok: true });
180
182
  });
181
183
 
184
+ // --- Role-to-Integration Mapping ---
185
+
186
+ app.get('/api/roles/integrations', (req, res) => {
187
+ const roleFilter = req.query.role;
188
+ const entries = roleFilter ? { [roleFilter]: ROLE_INTEGRATIONS[roleFilter] || [] } : ROLE_INTEGRATIONS;
189
+ const result = {};
190
+ for (const [role, ids] of Object.entries(entries)) {
191
+ result[role] = (ids || []).map((id) => {
192
+ const status = daemon.integrations.getStatus(id);
193
+ const entry = daemon.integrations.registry.find((r) => r.id === id);
194
+ return {
195
+ id,
196
+ name: entry?.name || id,
197
+ installed: status?.installed || false,
198
+ configured: status?.configured || false,
199
+ authenticated: status?.authenticated || false,
200
+ };
201
+ });
202
+ }
203
+ if (roleFilter) return res.json(result[roleFilter] || []);
204
+ res.json(result);
205
+ });
206
+
207
+ app.post('/api/agents/preflight', (req, res) => {
208
+ const { role, integrations } = req.body || {};
209
+ if (!role || !Array.isArray(integrations)) {
210
+ return res.status(400).json({ error: 'role and integrations[] required' });
211
+ }
212
+ const issues = [];
213
+ for (const id of integrations) {
214
+ const status = daemon.integrations.getStatus(id);
215
+ const entry = daemon.integrations.registry.find((r) => r.id === id);
216
+ const name = entry?.name || id;
217
+ if (!status || !status.installed) {
218
+ issues.push({ integrationId: id, name, problem: 'not_installed' });
219
+ } else if (!status.configured) {
220
+ issues.push({ integrationId: id, name, problem: 'not_configured' });
221
+ } else if (!status.authenticated) {
222
+ issues.push({ integrationId: id, name, problem: 'not_authenticated' });
223
+ }
224
+ }
225
+ res.json({ ready: issues.length === 0, issues });
226
+ });
227
+
228
+ // --- Agent Integration Attach/Detach ---
229
+
230
+ app.post('/api/agents/:id/integrations/:integrationId', (req, res) => {
231
+ const agent = daemon.registry.get(req.params.id);
232
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
233
+
234
+ const integrationId = req.params.integrationId;
235
+ const status = daemon.integrations.getStatus(integrationId);
236
+ if (!status || !status.installed) {
237
+ return res.status(400).json({ error: `Integration not installed: ${integrationId}` });
238
+ }
239
+
240
+ const integrations = new Set(agent.integrations || []);
241
+ integrations.add(integrationId);
242
+ const updated = Array.from(integrations);
243
+
244
+ daemon.registry.update(req.params.id, { integrations: updated });
245
+ daemon.integrations.writeMcpJson(daemon.integrations.getActiveIntegrations());
246
+ daemon.integrations.refreshMcpJson();
247
+ daemon.audit.log('agent.integration.attach', { agentId: req.params.id, integrationId });
248
+ daemon.broadcast({ type: 'agent:integration:attach', agentId: req.params.id, integrationId });
249
+ res.json({ ok: true, integrations: updated });
250
+ });
251
+
252
+ app.delete('/api/agents/:id/integrations/:integrationId', (req, res) => {
253
+ const agent = daemon.registry.get(req.params.id);
254
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
255
+
256
+ const integrationId = req.params.integrationId;
257
+ const integrations = (agent.integrations || []).filter((id) => id !== integrationId);
258
+
259
+ daemon.registry.update(req.params.id, { integrations });
260
+ daemon.integrations.refreshMcpJson();
261
+ daemon.audit.log('agent.integration.detach', { agentId: req.params.id, integrationId });
262
+ daemon.broadcast({ type: 'agent:integration:detach', agentId: req.params.id, integrationId });
263
+ res.json({ ok: true, integrations });
264
+ });
265
+
182
266
  // Lock management
183
267
  app.get('/api/locks', (req, res) => {
184
268
  res.json(daemon.locks.getAll());
@@ -307,10 +391,25 @@ export function createApi(app, daemon) {
307
391
  // Enrich with credential status
308
392
  for (const p of providers) {
309
393
  p.hasKey = daemon.credentials.hasKey(p.id);
394
+ if (p.id === 'claude-code') {
395
+ p.authStatus = ClaudeCodeProvider.getAuthStatus();
396
+ }
310
397
  }
311
398
  res.json(providers);
312
399
  });
313
400
 
401
+ // --- Claude Code Auth ---
402
+
403
+ app.get('/api/providers/claude-code/auth', (req, res) => {
404
+ res.json(ClaudeCodeProvider.getAuthStatus());
405
+ });
406
+
407
+ app.post('/api/providers/claude-code/login', (req, res) => {
408
+ ClaudeCodeProvider.triggerLogin();
409
+ daemon.audit.log('claude-code.login.started', {});
410
+ res.json({ ok: true });
411
+ });
412
+
314
413
  // --- Ollama ---
315
414
 
316
415
  app.get('/api/providers/ollama/hardware', (req, res) => {
@@ -14,7 +14,7 @@ export class Introducer {
14
14
  }
15
15
 
16
16
  generateContext(newAgent, options = {}) {
17
- const { taskNegotiation, hasTask } = options;
17
+ const { taskNegotiation, hasTask, isRotation } = options;
18
18
  const agents = this.daemon.registry.getAll();
19
19
  // Only include ACTIVE agents — not completed/killed ones from previous sessions
20
20
  // Completed agents' work is captured in the journalist's project map, not here
@@ -353,13 +353,13 @@ export class Introducer {
353
353
  parts.push(`### Constraints (read carefully)\n${constraints}`);
354
354
  }
355
355
 
356
- if (hasTask) {
357
- const discoveries = this.daemon.memory.getDiscoveriesMarkdown(newAgent.role, 15, 1000);
356
+ if (hasTask || isRotation) {
357
+ const discoveries = this.daemon.memory.getDiscoveriesMarkdown(newAgent.role, 8, 600, newAgent.scope);
358
358
  if (discoveries) {
359
359
  parts.push(`### Known Fixes for ${newAgent.role} Role\n${discoveries}`);
360
360
  }
361
361
 
362
- const handoffs = this.daemon.memory.getRecentHandoffMarkdown(newAgent.role, 2, 1000, newAgent.workingDir);
362
+ const handoffs = this.daemon.memory.getRecentHandoffMarkdown(newAgent.role, 2, 1000, newAgent.workingDir, newAgent.teamId);
363
363
  if (handoffs) {
364
364
  parts.push(`### Recent Handoff History\n${handoffs}`);
365
365
  }
@@ -367,9 +367,9 @@ export class Introducer {
367
367
 
368
368
  if (parts.length > 0) {
369
369
  memorySection = `\n## Project Memory (auto-generated)\n\n${parts.join('\n\n')}\n`;
370
- // Hard budget: 4K chars total
371
- if (memorySection.length > 4000) {
372
- memorySection = memorySection.slice(0, 3997) + '...';
370
+ // Hard budget: 3K chars total
371
+ if (memorySection.length > 3000) {
372
+ memorySection = memorySection.slice(0, 2997) + '...';
373
373
  }
374
374
  }
375
375
  }
@@ -6,8 +6,9 @@ import { resolve } from 'path';
6
6
  import { execFile, spawn as cpSpawn } from 'child_process';
7
7
  import { getProvider, getInstalledProviders } from './providers/index.js';
8
8
 
9
- const DEFAULT_INTERVAL = 120_000; // 2 minutes
9
+ const DEFAULT_INTERVAL = 300_000; // 5 minutes (safety-net fallback; event-driven triggers handle the normal case)
10
10
  const MAX_LOG_CHARS = 100_000; // ~25k tokens budget for synthesis input (captures 80-90% of recent activity)
11
+ const DEBOUNCE_MS = 10_000; // requestSynthesis debounce window
11
12
 
12
13
  export class Journalist {
13
14
  constructor(daemon) {
@@ -19,6 +20,8 @@ export class Journalist {
19
20
  this.synthesizing = false;
20
21
  this.lastSynthesis = null; // last synthesis result text
21
22
  this.history = []; // recent synthesis summaries
23
+ this._debounceTimer = null;
24
+ this._debounceReason = null;
22
25
  }
23
26
 
24
27
  start(intervalMs = DEFAULT_INTERVAL) {
@@ -51,6 +54,33 @@ export class Journalist {
51
54
  clearInterval(this.interval);
52
55
  this.interval = null;
53
56
  }
57
+ if (this._debounceTimer) {
58
+ clearTimeout(this._debounceTimer);
59
+ this._debounceTimer = null;
60
+ }
61
+ }
62
+
63
+ requestSynthesis(reason = 'unknown') {
64
+ if (this._debounceTimer) {
65
+ this._debounceReason = reason;
66
+ return;
67
+ }
68
+ this._debounceReason = reason;
69
+ this._debounceTimer = setTimeout(() => {
70
+ const r = this._debounceReason;
71
+ this._debounceTimer = null;
72
+ this._debounceReason = null;
73
+ this.cycle().catch((err) => {
74
+ console.error(` Journalist requestSynthesis(${r}) failed:`, err.message);
75
+ });
76
+ }, DEBOUNCE_MS);
77
+ }
78
+
79
+ async ensureFresh(maxAgeMs = 30000) {
80
+ if (this.lastCycleAt && (Date.now() - this.lastCycleAt) < maxAgeMs) {
81
+ return;
82
+ }
83
+ await this.cycle();
54
84
  }
55
85
 
56
86
  async cycle() {
@@ -454,11 +484,11 @@ export class Journalist {
454
484
  }
455
485
  const provider = getProvider(providerId);
456
486
 
457
- // Pick the lightest model for synthesis (cheapest/fastest)
458
- const lightModel = provider.constructor.models?.find((m) => m.tier === 'light')
459
- || provider.constructor.models?.find((m) => m.tier === 'medium')
487
+ // Pick medium tier for higher-quality synthesis (fewer but better cycles)
488
+ const selectedModel = provider.constructor.models?.find((m) => m.tier === 'medium')
489
+ || provider.constructor.models?.find((m) => m.tier === 'light')
460
490
  || provider.constructor.models?.[0];
461
- const modelId = lightModel?.id || null;
491
+ const modelId = selectedModel?.id || null;
462
492
 
463
493
  const headlessCmd = provider.buildHeadlessCommand(prompt, modelId);
464
494
  const { command, args, env, stdin: stdinData } = headlessCmd;
@@ -749,7 +779,7 @@ export class Journalist {
749
779
  // Pull recent rotation history from persistent memory (Layer 7).
750
780
  // Gives the new agent causal continuity: what the last 3 agents struggled
751
781
  // with, decided, and solved — not just what the current session did.
752
- const recentChain = this.daemon.memory?.getRecentHandoffMarkdown(agent.role, 3, 3000, agent.workingDir) || '';
782
+ const recentChain = this.daemon.memory?.getRecentHandoffMarkdown(agent.role, 3, 3000, agent.workingDir, agent.teamId) || '';
753
783
 
754
784
  // Pull the user's recent messages scoped to this agent
755
785
  const agentFeedback = this.getUserFeedback(agent.id).slice(-5);
@@ -12,6 +12,7 @@
12
12
  import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, appendFileSync, statSync } from 'fs';
13
13
  import { resolve, relative } from 'path';
14
14
  import { createHash } from 'crypto';
15
+ import { minimatch } from 'minimatch';
15
16
 
16
17
  const MAX_CONSTRAINTS = 50;
17
18
  const MAX_HANDOFF_ROTATIONS = 25;
@@ -141,7 +142,12 @@ export class MemoryStore {
141
142
  return safeName(rel);
142
143
  }
143
144
 
144
- _chainPath(role, workingDir) {
145
+ _chainPath(role, workingDir, teamId) {
146
+ if (teamId) {
147
+ const dir = resolve(this.handoffDir, safeName(teamId));
148
+ mkdirSync(dir, { recursive: true });
149
+ return resolve(dir, `${safeName(role)}.md`);
150
+ }
145
151
  const slug = this._workspaceSlug(workingDir);
146
152
  if (slug) {
147
153
  const dir = resolve(this.handoffDir, slug);
@@ -151,8 +157,8 @@ export class MemoryStore {
151
157
  return resolve(this.handoffDir, `${safeName(role)}.md`);
152
158
  }
153
159
 
154
- getHandoffChain(role, workingDir) {
155
- const path = this._chainPath(role, workingDir);
160
+ getHandoffChain(role, workingDir, teamId) {
161
+ const path = this._chainPath(role, workingDir, teamId);
156
162
  if (!existsSync(path)) return [];
157
163
  try {
158
164
  const content = readFileSync(path, 'utf8');
@@ -173,9 +179,9 @@ export class MemoryStore {
173
179
  }
174
180
  }
175
181
 
176
- appendHandoffBrief(role, entry, workingDir) {
182
+ appendHandoffBrief(role, entry, workingDir, teamId) {
177
183
  if (!role || !entry) return false;
178
- const chain = this.getHandoffChain(role, workingDir);
184
+ const chain = this.getHandoffChain(role, workingDir, teamId);
179
185
  const nextN = (chain[0]?.rotationN || 0) + 1;
180
186
 
181
187
  const block = [
@@ -203,15 +209,15 @@ export class MemoryStore {
203
209
  }
204
210
 
205
211
  try {
206
- writeFileSync(this._chainPath(role, workingDir), lines.join('\n'));
212
+ writeFileSync(this._chainPath(role, workingDir, teamId), lines.join('\n'));
207
213
  return true;
208
214
  } catch {
209
215
  return false;
210
216
  }
211
217
  }
212
218
 
213
- getRecentHandoffMarkdown(role, count = 3, maxChars = 4000, workingDir) {
214
- const chain = this.getHandoffChain(role, workingDir);
219
+ getRecentHandoffMarkdown(role, count = 3, maxChars = 4000, workingDir, teamId) {
220
+ const chain = this.getHandoffChain(role, workingDir, teamId);
215
221
  if (chain.length === 0) return '';
216
222
  const recent = chain.slice(0, count);
217
223
  const out = recent.map((e) => e.body || '').join('\n\n---\n\n');
@@ -300,9 +306,22 @@ export class MemoryStore {
300
306
  } catch { /* best-effort */ }
301
307
  }
302
308
 
303
- getDiscoveriesMarkdown(role, limit = 20, maxChars = 4000) {
304
- const entries = this.listDiscoveries({ role, limit });
309
+ getDiscoveriesMarkdown(role, limit = 20, maxChars = 4000, scope) {
310
+ let entries = this.listDiscoveries({ role, limit: limit * 3 });
305
311
  if (entries.length === 0) return '';
312
+
313
+ if (scope && Array.isArray(scope) && scope.length > 0) {
314
+ const filtered = entries.filter((d) => {
315
+ const file = d.fix || '';
316
+ const rel = file.startsWith(this.projectDir + '/') ? file.slice(this.projectDir.length + 1) : file;
317
+ return scope.some((pattern) => minimatch(rel, pattern, { dot: true }));
318
+ });
319
+ if (filtered.length >= 3) {
320
+ entries = filtered;
321
+ }
322
+ }
323
+
324
+ entries = entries.slice(0, limit);
306
325
  const lines = entries.map((d) => `- When \`${d.trigger}\` → fix: ${d.fix}`);
307
326
  return truncate(lines.join('\n'), maxChars);
308
327
  }
@@ -266,6 +266,18 @@ IMPORTANT: Do not use markdown formatting like ** or ### in your output. Write i
266
266
  `,
267
267
  };
268
268
 
269
+ // Role-to-integration mapping — recommended integrations per role for onboarding preflight
270
+ export const ROLE_INTEGRATIONS = {
271
+ ea: ['gmail', 'google-calendar'],
272
+ cmo: ['gmail', 'slack', 'hubspot'],
273
+ cfo: ['stripe', 'google-sheets'],
274
+ support: ['gmail', 'slack', 'zendesk'],
275
+ analyst: ['google-sheets', 'postgres', 'mixpanel'],
276
+ home: ['home-assistant'],
277
+ slides: ['google-slides'],
278
+ creative: ['google-docs'],
279
+ };
280
+
269
281
  // Permission-level prompt instructions
270
282
  // "auto" = PM reviews risky ops via API. "full" = no reviews, max speed.
271
283
  const PERMISSION_PROMPTS = {
@@ -427,12 +439,18 @@ export class ProcessManager {
427
439
  taskNegotiation = await this.negotiateTaskSplit(agent, sameRole);
428
440
  }
429
441
 
430
- // Generate introduction context (team awareness + negotiation)
431
- // Always pass hasTask: true so Layer 7 discoveries and handoff history
432
- // are injected for ALL agents, not just those with explicit prompts.
433
- // Without this, first-generation agents spawned with just a role never
434
- // receive prior discoveries and repeat mistakes Layer 7 already captured.
435
- const introContext = introducer.generateContext(agent, { taskNegotiation, hasTask: true });
442
+ // Compute hasTask from actual prompt content — agents spawned without a
443
+ // prompt should NOT receive handoff history (prevents cross-team contamination).
444
+ // Discoveries + constraints are always injected (project knowledge).
445
+ // Handoffs are injected only when the agent has a real task or is a rotation.
446
+ const hasTask = !!(config.prompt && config.prompt.trim().length > 0);
447
+ const isRotation = !!(config.isRotation);
448
+ const introContext = introducer.generateContext(agent, { taskNegotiation, hasTask, isRotation });
449
+
450
+ // Ensure the project map is fresh before the new agent reads CLAUDE.md
451
+ if (this.daemon.journalist) {
452
+ await this.daemon.journalist.ensureFresh(30000);
453
+ }
436
454
 
437
455
  // Track cold-start savings — agent gets context from planner/journalist/team
438
456
  // instead of exploring the codebase from scratch
@@ -597,7 +615,7 @@ For normal file edits within your scope, proceed without review.
597
615
 
598
616
  this.daemon.broadcast({ type: 'agent:exit', agentId: agent.id, code: code || 0, signal, status });
599
617
  if (this.daemon.integrations) this.daemon.integrations.refreshMcpJson();
600
- if (status === 'completed' && this.daemon.journalist) this.daemon.journalist.cycle().catch(() => {});
618
+ if (status === 'completed' && this.daemon.journalist) this.daemon.journalist.requestSynthesis('completion');
601
619
  this._checkPhase2(agent.id);
602
620
 
603
621
  // Auto-trigger idle QC + process cross-scope handoffs
@@ -783,10 +801,9 @@ For normal file edits within your scope, proceed without review.
783
801
  }
784
802
  }
785
803
 
786
- // Trigger journalist synthesis immediately on completion so the project
787
- // map is fresh for the next agent that spawns (don't wait for 120s cycle)
804
+ // Trigger journalist synthesis on completion (event-driven, debounced)
788
805
  if (finalStatus === 'completed' && this.daemon.journalist) {
789
- this.daemon.journalist.cycle().catch(() => {});
806
+ this.daemon.journalist.requestSynthesis('completion');
790
807
  }
791
808
 
792
809
  // Phase 2 auto-spawn: check if all phase 1 agents for a team are done
@@ -1168,7 +1185,7 @@ For normal file edits within your scope, proceed without review.
1168
1185
  oldTokens: agentData?.tokensUsed || 0,
1169
1186
  contextUsage: agentData?.contextUsage || 0,
1170
1187
  brief: brief.slice(0, 4000),
1171
- }, agent.workingDir);
1188
+ }, agent.workingDir, agent.teamId);
1172
1189
  } catch { /* best-effort */ }
1173
1190
  }
1174
1191
 
@@ -1369,7 +1386,7 @@ For normal file edits within your scope, proceed without review.
1369
1386
  registry.update(newAgent.id, { status: finalStatus, pid: null });
1370
1387
  this.daemon.broadcast({ type: 'agent:exit', agentId: newAgent.id, code, signal, status: finalStatus });
1371
1388
  if (finalStatus === 'completed' && this.daemon.journalist) {
1372
- this.daemon.journalist.cycle().catch(() => {});
1389
+ this.daemon.journalist.requestSynthesis('completion');
1373
1390
  }
1374
1391
  });
1375
1392
 
@@ -1,7 +1,7 @@
1
1
  // GROOVE — Claude Code Provider
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import { execSync } from 'child_process';
4
+ import { execSync, spawn as cpSpawn } from 'child_process';
5
5
  import { writeFileSync, readFileSync, existsSync } from 'fs';
6
6
  import { resolve } from 'path';
7
7
  import { homedir } from 'os';
@@ -223,4 +223,29 @@ export class ClaudeCodeProvider extends Provider {
223
223
 
224
224
  return merged;
225
225
  }
226
+
227
+ static getAuthStatus() {
228
+ try {
229
+ const out = execSync('claude auth status --json', { encoding: 'utf8', timeout: 10_000, stdio: ['pipe', 'pipe', 'pipe'] });
230
+ const data = JSON.parse(out);
231
+ return {
232
+ authenticated: true,
233
+ authMethod: data.authMethod || data.auth_method || 'unknown',
234
+ email: data.email || null,
235
+ subscriptionType: data.subscriptionType || data.subscription_type || null,
236
+ orgName: data.orgName || data.org_name || null,
237
+ };
238
+ } catch (err) {
239
+ return { authenticated: false, error: err.message };
240
+ }
241
+ }
242
+
243
+ static triggerLogin() {
244
+ const child = cpSpawn('claude', ['auth', 'login', '--claudeai'], {
245
+ detached: true,
246
+ stdio: 'ignore',
247
+ });
248
+ child.unref();
249
+ return { pid: child.pid };
250
+ }
226
251
  }