groove-dev 0.26.21 → 0.26.23

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 (25) hide show
  1. package/node_modules/@groove-dev/cli/src/commands/start.js +2 -1
  2. package/node_modules/@groove-dev/daemon/src/introducer.js +8 -2
  3. package/node_modules/@groove-dev/daemon/src/process.js +94 -32
  4. package/node_modules/@groove-dev/gui/dist/assets/index-D9JZfCf8.css +1 -0
  5. package/node_modules/@groove-dev/gui/dist/assets/{index-DlAgOHr2.js → index-Ebz1No43.js} +26 -26
  6. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  7. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +5 -5
  8. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +4 -4
  9. package/node_modules/@groove-dev/gui/src/components/ui/thinking-indicator.jsx +35 -73
  10. package/node_modules/@groove-dev/gui/src/stores/groove.js +3 -2
  11. package/node_modules/@groove-dev/gui/src/views/agents.jsx +49 -18
  12. package/package.json +1 -1
  13. package/packages/cli/src/commands/start.js +2 -1
  14. package/packages/daemon/src/introducer.js +8 -2
  15. package/packages/daemon/src/process.js +94 -32
  16. package/packages/gui/dist/assets/index-D9JZfCf8.css +1 -0
  17. package/packages/gui/dist/assets/{index-DlAgOHr2.js → index-Ebz1No43.js} +26 -26
  18. package/packages/gui/dist/index.html +2 -2
  19. package/packages/gui/src/components/agents/agent-feed.jsx +5 -5
  20. package/packages/gui/src/components/agents/agent-node.jsx +4 -4
  21. package/packages/gui/src/components/ui/thinking-indicator.jsx +35 -73
  22. package/packages/gui/src/stores/groove.js +3 -2
  23. package/packages/gui/src/views/agents.jsx +49 -18
  24. package/node_modules/@groove-dev/gui/dist/assets/index-BgX9uAx-.css +0 -1
  25. package/packages/gui/dist/assets/index-BgX9uAx-.css +0 -1
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/png" href="/favicon.png" />
7
7
  <title>Groove GUI</title>
8
- <script type="module" crossorigin src="/assets/index-DlAgOHr2.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-Ebz1No43.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-BgX9uAx-.css">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-D9JZfCf8.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
@@ -312,9 +312,9 @@ function ActivityGroup({ entries, isLive }) {
312
312
  const meta = activityMeta(last.text);
313
313
  const Icon = meta.icon;
314
314
  return (
315
- <div className="w-fit flex items-center gap-2 px-3 py-1 text-[10px] text-text-4 font-mono">
315
+ <div className="inline-flex items-center gap-2 px-3 py-1 text-[10px] text-text-4 font-mono">
316
316
  <Icon size={10} className="opacity-50" />
317
- <span className="truncate">{entries.length} tool call{entries.length !== 1 ? 's' : ''}</span>
317
+ <span>{entries.length} tool call{entries.length !== 1 ? 's' : ''}</span>
318
318
  </div>
319
319
  );
320
320
  }
@@ -323,9 +323,9 @@ function ActivityGroup({ entries, isLive }) {
323
323
  const display = current.text?.length > 60 ? current.text.slice(0, 60) + '...' : current.text;
324
324
 
325
325
  return (
326
- <div className="w-fit flex items-center gap-2 px-3 py-2 rounded-md bg-surface-3/50 border border-border-subtle/30">
326
+ <div className="inline-flex items-center gap-2 px-3 py-2 max-w-[280px] rounded-md bg-surface-3/50 border border-border-subtle/30">
327
327
  <Loader2 size={11} className="text-accent animate-spin flex-shrink-0" />
328
- <span className="text-[11px] text-text-2 font-mono truncate flex-1 min-w-0 transition-opacity duration-300">
328
+ <span className="text-[11px] text-text-2 font-mono truncate transition-opacity duration-300">
329
329
  {display}
330
330
  </span>
331
331
  {entries.length > 1 && (
@@ -626,7 +626,7 @@ export function AgentFeed({ agent }) {
626
626
  if (item.kind === 'activity-group') {
627
627
  // Only the last activity group is "live" if agent is still running
628
628
  const isLastGroup = !timeline.slice(i + 1).some((t) => t.kind === 'activity-group' || t.from === 'agent');
629
- return <ActivityGroup key={`grp-${i}`} entries={item.entries} isLive={isAlive && isLastGroup} />;
629
+ return <div key={`grp-${i}`}><ActivityGroup entries={item.entries} isLive={isAlive && isLastGroup} /></div>;
630
630
  }
631
631
  if (item.from === 'user') return <UserMessage key={`msg-${i}`} msg={item} />;
632
632
  if (item.from === 'system') return <SystemMessage key={`msg-${i}`} msg={item} />;
@@ -195,14 +195,14 @@ const AgentNode = memo(({ data, selected }) => {
195
195
  </div>
196
196
 
197
197
  {/* Context bar */}
198
- <div className="mt-1.5 h-[2px] rounded-sm bg-[#262a32] overflow-hidden">
198
+ <div className="mt-1.5 h-[2px] rounded-sm bg-[#3e4451] overflow-hidden">
199
199
  <div
200
200
  className="h-full rounded-sm transition-all duration-700"
201
201
  style={{
202
202
  width: `${Math.max(contextPct, 1)}%`,
203
- background: contextPct > 80 ? '#e06c75'
204
- : contextPct > 60 ? '#e5c07b'
205
- : isAlive ? '#61afef' : '#333842',
203
+ background: contextPct > 80 ? 'var(--color-danger)'
204
+ : contextPct > 60 ? 'var(--color-warning)'
205
+ : isAlive ? 'var(--color-accent)' : '#3e4451',
206
206
  }}
207
207
  />
208
208
  </div>
@@ -1,89 +1,51 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useState, useEffect } from 'react';
3
3
 
4
- const PHASES = [
5
- 'Analyzing codebase',
6
- 'Reading context',
7
- 'Planning approach',
8
- 'Reasoning through solution',
9
- 'Evaluating options',
10
- ];
11
-
12
- const DETAILS = [
13
- ['Scanning project structure...', 'Checking dependencies...', 'Reviewing recent changes...'],
14
- ['Parsing file tree...', 'Loading imports...', 'Indexing symbols...'],
15
- ['Mapping task scope...', 'Identifying constraints...', 'Outlining steps...'],
16
- ['Tracing logic flow...', 'Considering edge cases...', 'Weighing approaches...'],
17
- ['Comparing alternatives...', 'Checking tradeoffs...', 'Selecting best path...'],
4
+ const MESSAGES = [
5
+ 'Reading through the codebase...',
6
+ 'Thinking through your request...',
7
+ 'Planning the approach...',
8
+ 'Running tool calls...',
9
+ 'Working through the problem...',
10
+ 'Reasoning step by step...',
11
+ 'Reviewing context...',
12
+ 'Considering options...',
13
+ 'Analyzing the code...',
14
+ 'Making progress...',
18
15
  ];
19
16
 
20
17
  export function ThinkingIndicator({ agent, className }) {
21
- const [phase, setPhase] = useState(0);
22
- const [elapsed, setElapsed] = useState(0);
23
-
24
- // Cycle phases every 3.5s
25
- useEffect(() => {
26
- const t = setInterval(() => setPhase((p) => (p + 1) % PHASES.length), 3500);
27
- return () => clearInterval(t);
28
- }, []);
18
+ const [idx, setIdx] = useState(0);
19
+ const [fade, setFade] = useState(true);
29
20
 
30
- // Elapsed timer
31
21
  useEffect(() => {
32
- const t = setInterval(() => setElapsed((e) => e + 1), 1000);
22
+ const t = setInterval(() => {
23
+ setFade(false);
24
+ setTimeout(() => {
25
+ setIdx((i) => (i + 1) % MESSAGES.length);
26
+ setFade(true);
27
+ }, 250);
28
+ }, 2800);
33
29
  return () => clearInterval(t);
34
30
  }, []);
35
31
 
36
- const secs = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
37
-
38
32
  return (
39
- <div className={`relative rounded-xl bg-surface-2 border border-border-subtle overflow-hidden my-2 ${className || ''}`}>
40
- {/* Shimmer sweep */}
41
- <div className="absolute inset-0 bg-gradient-to-r from-transparent via-accent/[0.04] to-transparent animate-shimmer pointer-events-none" />
42
-
43
- <div className="relative px-4 py-4 min-h-[90px]">
44
- {/* Header: spinning ring + identity + timer */}
45
- <div className="flex items-center gap-3 mb-4">
46
- {/* Spinning ring same pattern as BootSequence */}
47
- <div className="relative w-8 h-8 flex-shrink-0">
48
- <span className="absolute inset-0 rounded-full border-2 border-accent/15 animate-ping" style={{ animationDuration: '2.5s' }} />
49
- <span className="absolute inset-0 rounded-full border-2 border-transparent border-t-accent animate-spin" style={{ animationDuration: '1.2s' }} />
50
- <span className="absolute inset-[6px] rounded-full bg-accent/10" />
51
- </div>
52
-
53
- <div className="flex-1 min-w-0">
54
- {agent ? (
55
- <>
56
- <p className="text-xs font-sans font-semibold text-text-1 truncate leading-tight">{agent.name}</p>
57
- <p className="text-[10px] font-mono text-accent leading-tight mt-0.5">thinking</p>
58
- </>
59
- ) : (
60
- <p className="text-xs font-sans font-semibold text-text-1 leading-tight">Thinking</p>
61
- )}
62
- </div>
63
-
64
- <span className="text-xs font-mono text-text-2 tabular-nums flex-shrink-0">{secs}</span>
65
- </div>
66
-
67
- {/* Phase label — re-keyed to replay fade-in on phase change */}
68
- <div className="pl-3 border-l-2 border-accent/25 mb-3">
69
- <span key={phase} className="block text-[13px] font-sans font-medium text-text-1 animate-phase-in">
70
- {PHASES[phase]}
71
- </span>
72
- </div>
73
-
74
- {/* Staggered detail lines — re-keyed to replay cascade on phase change */}
75
- <div className="space-y-1.5 pl-3 border-l border-border-subtle">
76
- {DETAILS[phase].map((line, i) => (
77
- <div
78
- key={`${phase}-${i}`}
79
- className="flex items-center gap-2 animate-cascade-in"
80
- style={{ animationDelay: `${150 + i * 200}ms` }}
81
- >
82
- <span className="w-1 h-1 rounded-full bg-accent/35 flex-shrink-0" />
83
- <span className="text-[11px] font-mono text-text-3">{line}</span>
84
- </div>
85
- ))}
33
+ <div className={`${className || ''}`}>
34
+ <div className="flex items-center gap-2 mb-1">
35
+ <span className="text-2xs font-semibold text-text-1 font-sans">{agent?.name || 'Agent'}</span>
36
+ <span className="text-2xs text-accent font-mono">thinking</span>
37
+ </div>
38
+ <div className="border-l border-accent/40 pl-3.5 py-1 flex items-center gap-2.5">
39
+ {/* Spinning ring */}
40
+ <div className="relative w-3.5 h-3.5 flex-shrink-0">
41
+ <span className="absolute inset-0 rounded-full border border-transparent border-t-accent animate-spin" style={{ animationDuration: '0.9s' }} />
86
42
  </div>
43
+ <span
44
+ className="text-[12px] font-sans text-text-3 transition-opacity duration-[250ms]"
45
+ style={{ opacity: fade ? 1 : 0 }}
46
+ >
47
+ {MESSAGES[idx]}
48
+ </span>
87
49
  </div>
88
50
  </div>
89
51
  );
@@ -550,11 +550,12 @@ export const useGrooveStore = create((set, get) => ({
550
550
  }
551
551
  },
552
552
 
553
- async launchRecommendedTeam() {
553
+ async launchRecommendedTeam(modifiedAgents) {
554
554
  try {
555
555
  set({ recommendedTeam: null }); // Dismiss modal immediately
556
556
  get().addToast('info', 'Launching team...');
557
- const result = await api.post('/recommended-team/launch');
557
+ const body = modifiedAgents ? { agents: modifiedAgents } : undefined;
558
+ const result = await api.post('/recommended-team/launch', body);
558
559
  const sub = [
559
560
  result.phase2Pending ? `${result.phase2Pending} QC queued` : '',
560
561
  result.projectDir ? `→ ${result.projectDir}/` : '',
@@ -474,11 +474,17 @@ function EmptyState({ onPlanner, onSpawn }) {
474
474
 
475
475
  const ROLE_ICONS = { backend: Server, frontend: Monitor, fullstack: Code2, testing: TestTube, security: Shield };
476
476
 
477
+ const NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
478
+
479
+ function sanitizeName(raw) {
480
+ return raw.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64);
481
+ }
482
+
477
483
  function RecommendedTeamCard() {
478
484
  const recommendedTeam = useGrooveStore((s) => s.recommendedTeam);
479
485
  const launchRecommendedTeam = useGrooveStore((s) => s.launchRecommendedTeam);
480
- const checkRecommendedTeam = useGrooveStore((s) => s.checkRecommendedTeam);
481
486
  const [launching, setLaunching] = useState(false);
487
+ const [editedAgents, setEditedAgents] = useState(null);
482
488
 
483
489
  if (!recommendedTeam?.agents?.length) return null;
484
490
 
@@ -486,10 +492,23 @@ function RecommendedTeamCard() {
486
492
  const phase1 = agents.filter((a) => !a.phase || a.phase === 1);
487
493
  const phase2 = agents.filter((a) => a.phase === 2);
488
494
 
495
+ // Initialize edits lazily so we get fresh data if recommendedTeam changes
496
+ const agentEdits = editedAgents ?? phase1.map((a) => ({ ...a, name: a.name || '' }));
497
+
498
+ function handleNameChange(i, raw) {
499
+ const next = agentEdits.map((a, idx) => idx === i ? { ...a, name: sanitizeName(raw) } : a);
500
+ setEditedAgents(next);
501
+ }
502
+
489
503
  async function handleLaunch() {
490
504
  setLaunching(true);
491
505
  try {
492
- await launchRecommendedTeam();
506
+ // Merge edited phase1 names back with phase2 agents
507
+ const modified = [
508
+ ...agentEdits,
509
+ ...phase2,
510
+ ];
511
+ await launchRecommendedTeam(modified);
493
512
  } catch { /* toast handles */ }
494
513
  setLaunching(false);
495
514
  }
@@ -507,26 +526,38 @@ function RecommendedTeamCard() {
507
526
  <button onClick={handleDismiss} className="text-text-4 hover:text-text-1 cursor-pointer"><X size={14} /></button>
508
527
  </div>
509
528
 
510
- <div className="px-4 py-3 space-y-2">
511
- {/* Phase 1 agents */}
512
- <div className="flex flex-wrap gap-2">
513
- {phase1.map((a, i) => {
514
- const Icon = ROLE_ICONS[a.role] || Code2;
515
- return (
516
- <div key={i} className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-surface-4 border border-border-subtle">
517
- <Icon size={12} className="text-text-2" />
518
- <span className="text-xs font-semibold text-text-0 font-sans capitalize">{a.name || a.role}</span>
519
- {a.scope?.length > 0 && (
520
- <span className="text-2xs text-text-4 font-mono">{a.scope[0]}{a.scope.length > 1 ? ` +${a.scope.length - 1}` : ''}</span>
529
+ <div className="px-4 py-3 space-y-1.5">
530
+ {/* Phase 1 agents — editable rows */}
531
+ {agentEdits.map((a, i) => {
532
+ const Icon = ROLE_ICONS[a.role] || Code2;
533
+ const nameValid = !a.name || NAME_RE.test(a.name);
534
+ return (
535
+ <div key={i} className="flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-surface-4 border border-border-subtle">
536
+ <Icon size={12} className="text-text-2 shrink-0" />
537
+ <input
538
+ type="text"
539
+ value={a.name}
540
+ onChange={(e) => handleNameChange(i, e.target.value)}
541
+ placeholder={a.role}
542
+ className={cn(
543
+ 'flex-1 min-w-0 bg-transparent text-xs font-mono text-text-0 outline-none placeholder:text-text-4',
544
+ !nameValid && 'text-red-400',
521
545
  )}
522
- </div>
523
- );
524
- })}
525
- </div>
546
+ maxLength={64}
547
+ spellCheck={false}
548
+ />
549
+ {a.scope?.length > 0 && (
550
+ <span className="text-2xs text-text-4 font-mono shrink-0 truncate max-w-[120px]">
551
+ {a.scope[0]}{a.scope.length > 1 ? ` +${a.scope.length - 1}` : ''}
552
+ </span>
553
+ )}
554
+ </div>
555
+ );
556
+ })}
526
557
 
527
558
  {/* Project dir indicator */}
528
559
  {recommendedTeam.projectDir && (
529
- <div className="flex items-center gap-1.5 text-2xs text-text-2 font-mono">
560
+ <div className="flex items-center gap-1.5 text-2xs text-text-2 font-mono pt-0.5">
530
561
  <span className="text-text-4">Project:</span>
531
562
  <span className="text-accent">{recommendedTeam.projectDir}/</span>
532
563
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.26.21",
3
+ "version": "0.26.23",
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)",
@@ -8,7 +8,7 @@ import chalk from 'chalk';
8
8
  import { runSetupWizard, saveKeysViaDaemon } from '../setup.js';
9
9
 
10
10
  export async function start(options) {
11
- const grooveDir = resolve(process.cwd(), '.groove');
11
+ const grooveDir = process.env.GROOVE_DIR || resolve(process.cwd(), '.groove');
12
12
  const isFirstRun = !existsSync(resolve(grooveDir, 'config.json'));
13
13
 
14
14
  // ── First-run interactive wizard ────────────────────────────
@@ -34,6 +34,7 @@ export async function start(options) {
34
34
  const daemon = new Daemon({
35
35
  port: parseInt(options.port, 10),
36
36
  host: options.host,
37
+ grooveDir: process.env.GROOVE_DIR || undefined,
37
38
  });
38
39
 
39
40
  const shutdown = async () => {
@@ -75,8 +75,14 @@ export class Introducer {
75
75
  lines.push('');
76
76
  lines.push(`## Coordination Rules`);
77
77
  lines.push('');
78
- lines.push(`- Stay within your file scope. Do NOT modify files owned by other agents.`);
79
- lines.push(`- If you need changes outside your scope, document what you need GROOVE will coordinate.`);
78
+ lines.push(`- Stay within your file scope when other agents are actively running.`);
79
+ lines.push(`- If you are the ONLY active agent, you may edit files outside your scope if needed to complete your task.`);
80
+ lines.push(`- If you need another agent to make changes (e.g., you're a frontend agent and need backend API changes):`);
81
+ lines.push(` Write a handoff file to .groove/handoffs/<role>.md (e.g., .groove/handoffs/backend.md) with:`);
82
+ lines.push(` - What needs to change and why`);
83
+ lines.push(` - Which files to modify`);
84
+ lines.push(` - Expected behavior after the change`);
85
+ lines.push(` GROOVE will automatically wake the target agent and deliver your request.`);
80
86
  lines.push(`- Check AGENTS_REGISTRY.md for the latest team state.`);
81
87
 
82
88
  // Project files section — tell the new agent what exists and what to read
@@ -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 } from 'fs';
5
+ import { createWriteStream, mkdirSync, chmodSync, existsSync, readFileSync, unlinkSync, readdirSync } from 'fs';
6
6
  import { resolve } from 'path';
7
7
  import { getProvider, getInstalledProviders } from './providers/index.js';
8
8
  import { AgentLoop } from './agent-loop.js';
@@ -76,27 +76,28 @@ For best results, apply a slide deck skill from the Marketplace. The skill provi
76
76
  Do NOT write code unless explicitly asked. Use your MCP tools to interact with Home Assistant.
77
77
 
78
78
  `,
79
- planner: `You are a PLANNING ONLY agent. You create plans. You do NOT write code, edit files, or run commands.
79
+ planner: `You are a PLANNING ONLY agent. You create plans and route work to your team. You do NOT write code, edit files, or run commands.
80
80
 
81
81
  ABSOLUTE RULE: Never use the Edit, Write, or Bash tools to modify source code. You ONLY use Read, Glob, and Grep to understand the codebase, then output a written plan. If the user says "build this" or "redesign this", create a PLAN for how other agents should build it — do NOT build it yourself.
82
82
 
83
- Focus on:
84
- - Understanding requirements
85
- - Exploring the codebase to understand current architecture
86
- - Identifying approaches and trade-offs
87
- - Writing structured plans with agent assignments
83
+ YOU HAVE TWO MODES:
88
84
 
89
- After completing your plan, you MUST do two things EVERY TIME, no exceptions:
85
+ MODE 1 TEAM CREATION (first time, no team exists yet):
86
+ Explore the codebase thoroughly, understand the architecture, then recommend a team structure.
90
87
 
91
- 1. Write your team recommendation as a clear summary in your output so the user can review it.
88
+ MODE 2 TASK ROUTING (team already exists):
89
+ Check AGENTS_REGISTRY.md or .groove/recommended-team.json to see your existing team.
90
+ Do NOT re-explore the entire codebase. You already know it from team creation.
91
+ Just read the specific files related to the bug/feature, decide which existing agent should handle it, and write the routing config. This should be FAST — under 5 tool calls.
92
92
 
93
- 2. Save a machine-readable team config to .groove/recommended-team.json using this EXACT format.
94
- ALWAYS write this file, even if the user says "no task yet" or "just building the team."
95
- For team-building without a specific task, use empty prompts ("prompt": "") the agents will wait for instructions.
93
+ HOW TO DETECT WHICH MODE:
94
+ - Read AGENTS_REGISTRY.md. If it lists agents with roles matching your team (frontend, backend, fullstack), you are in MODE 2.
95
+ - If no agents exist or only a planner exists, you are in MODE 1.
96
96
 
97
- For NEW projects (building something from scratch):
97
+ After completing your plan, you MUST write .groove/recommended-team.json — EVERY TIME, no exceptions.
98
+
99
+ For MODE 1 (team creation):
98
100
  {
99
- "projectDir": "my-project-name",
100
101
  "agents": [
101
102
  { "role": "frontend", "phase": 1, "scope": ["src/components/**", "src/views/**"], "prompt": "Build the frontend: [specific tasks]" },
102
103
  { "role": "backend", "phase": 1, "scope": ["src/api/**", "src/server/**"], "prompt": "Build the backend: [specific tasks]" },
@@ -104,35 +105,38 @@ For NEW projects (building something from scratch):
104
105
  ]
105
106
  }
106
107
 
107
- For EXISTING codebases (modifying/extending an existing project):
108
+ For MODE 2 (task routing to existing team):
109
+ Only include the agents that need to do work. Use their EXISTING role — the system will find and reuse them.
108
110
  {
109
111
  "agents": [
110
- { "role": "frontend", "phase": 1, "scope": ["src/components/**"], "prompt": "Update the frontend: [specific tasks]" },
111
- { "role": "fullstack", "phase": 2, "scope": [], "prompt": "QC Senior Dev: Audit all changes, fix issues, run tests, build, commit, and launch." }
112
+ { "role": "frontend", "phase": 1, "prompt": "Fix the bug: [specific description with file paths and what to change]" }
112
113
  ]
113
114
  }
115
+ Do NOT include QC/fullstack in the JSON for task routing — the system auto-triggers the existing QC when work completes.
116
+ Do NOT include agents that have no work to do.
117
+ Do NOT invent new agent names or roles — use the existing team's roles exactly.
118
+
119
+ For NEW projects (team creation only):
120
+ Include "projectDir" with a short kebab-case directory name. All agents spawn inside it.
121
+ For EXISTING codebases: Do NOT include "projectDir".
114
122
 
115
- PROJECT DIRECTORY RULES:
116
- - For NEW projects: ALWAYS include "projectDir" with a short, clean directory name (kebab-case, e.g. "cat-website", "landing-page", "api-service"). All agents will be spawned inside this directory so each project stays isolated.
117
- - For EXISTING codebases: Do NOT include "projectDir". Agents work in the current repo root. You can tell an existing codebase by the presence of package.json, .git, or established source directories.
118
- - NEVER mix projects. Each new project gets its own directory.
123
+ MANDATORY RULES:
119
124
 
120
- MANDATORY RULESNEVER SKIP THESE:
125
+ 1. For team creation: the LAST entry MUST be { "role": "fullstack", "phase": 2 } the QC agent.
126
+ For task routing: do NOT include the QC — it auto-triggers.
121
127
 
122
- 1. The LAST entry in the agents array MUST be: { "role": "fullstack", "phase": 2, ... }
123
- This is the QC Senior Dev. It auto-spawns after all other agents finish.
124
- Its prompt: audit changes, fix issues, run tests, build, commit, launch.
125
- NEVER omit this agent. Every team needs a QC.
128
+ 2. ALL phase 1 agents run in parallel. Do NOT tell agents to wait for each other.
126
129
 
127
- 2. ALL other agents are phase: 1 they run in parallel.
130
+ 3. If the user gave a specific task, write detailed prompts with file paths and what to change.
131
+ If no task was given, use empty prompts ("prompt": "") — agents will await instructions.
128
132
 
129
- 3. Do NOT tell any agent to "wait for" another agent. Phase 2 handles sequencing automatically.
133
+ 4. NEVER create new agent names or custom roles. Use the standard roles: frontend, backend, fullstack.
130
134
 
131
- 4. Set appropriate scopes. If the user gave a specific task, write detailed prompts. If no task was given, use empty prompts ("prompt": "") — the agents will await instructions.
135
+ 5. NEVER instruct agents to delete files from other projects or clean up unrelated code.
132
136
 
133
- 5. NEVER instruct any agent to delete files from other projects or clean up unrelated code. Each agent must ONLY create and modify files relevant to its assigned tasks.
137
+ 6. You MUST always write .groove/recommended-team.json. NEVER skip it.
134
138
 
135
- 6. You MUST always write .groove/recommended-team.json. NEVER skip it. The GUI depends on this file to show the Launch Team modal.
139
+ 7. In MODE 2, be FAST. Read only the files needed to understand the specific task. Do not re-analyze the full codebase.
136
140
 
137
141
  IMPORTANT: Do not use markdown formatting like ** or ### in your output. Write in plain text with clean formatting. Use line breaks, dashes, and indentation for structure.
138
142
 
@@ -373,10 +377,11 @@ For normal file edits within your scope, proceed without review.
373
377
  if (status === 'completed' && this.daemon.journalist) this.daemon.journalist.cycle().catch(() => {});
374
378
  this._checkPhase2(agent.id);
375
379
 
376
- // Auto-trigger idle QC in the same team
380
+ // Auto-trigger idle QC + process cross-scope handoffs
377
381
  if (status === 'completed') {
378
382
  const files = this.daemon.journalist?.getAgentFiles(agent) || [];
379
383
  if (files.length > 0) this._triggerIdleQC(agent);
384
+ this._processHandoffs(agent);
380
385
  }
381
386
  });
382
387
 
@@ -577,6 +582,8 @@ For normal file edits within your scope, proceed without review.
577
582
  if (finalStatus === 'completed') {
578
583
  const files = this.daemon.journalist?.getAgentFiles(agent) || [];
579
584
  if (files.length > 0) this._triggerIdleQC(agent);
585
+ // Process cross-scope handoff requests from this agent
586
+ this._processHandoffs(agent);
580
587
  }
581
588
  });
582
589
 
@@ -765,6 +772,61 @@ For normal file edits within your scope, proceed without review.
765
772
  });
766
773
  }
767
774
 
775
+ /**
776
+ * Process handoff files in .groove/handoffs/.
777
+ * Agents write handoff requests when they need cross-scope work from a teammate.
778
+ * File name = target role (e.g., backend.md). Content = what to do.
779
+ */
780
+ _processHandoffs(sourceAgent) {
781
+ const handoffsDir = resolve(this.daemon.grooveDir, 'handoffs');
782
+ if (!existsSync(handoffsDir)) return;
783
+
784
+ const registry = this.daemon.registry;
785
+ let files;
786
+ try { files = readdirSync(handoffsDir); } catch { return; }
787
+
788
+ for (const file of files) {
789
+ if (!file.endsWith('.md')) continue;
790
+ const targetRole = file.replace(/\.md$/, '');
791
+ const filePath = resolve(handoffsDir, file);
792
+
793
+ let content;
794
+ try { content = readFileSync(filePath, 'utf8').trim(); } catch { continue; }
795
+ if (!content) { try { unlinkSync(filePath); } catch {} continue; }
796
+
797
+ // Find the target agent in the same team
798
+ const target = registry.getAll().find((a) =>
799
+ a.role === targetRole &&
800
+ a.teamId === sourceAgent.teamId &&
801
+ a.id !== sourceAgent.id &&
802
+ (a.status === 'running' || a.status === 'completed')
803
+ );
804
+
805
+ if (!target) {
806
+ console.log(`[Groove] Handoff to ${targetRole} — no matching agent in team`);
807
+ try { unlinkSync(filePath); } catch {}
808
+ continue;
809
+ }
810
+
811
+ // Wake the target agent with the handoff request
812
+ const message = `Cross-scope handoff from ${sourceAgent.name} (${sourceAgent.role}):\n\n${content}`;
813
+ this.daemon.processes.resume(target.id, message).then((newAgent) => {
814
+ this.daemon.audit.log('handoff.routed', {
815
+ from: sourceAgent.name, to: target.name, newId: newAgent.id, role: targetRole,
816
+ });
817
+ this.daemon.broadcast({
818
+ type: 'handoff:routed',
819
+ from: sourceAgent.name, to: target.name, role: targetRole,
820
+ });
821
+ }).catch((err) => {
822
+ console.error(`[Groove] Handoff to ${targetRole} failed: ${err.message}`);
823
+ });
824
+
825
+ // Remove the handoff file
826
+ try { unlinkSync(filePath); } catch {}
827
+ }
828
+ }
829
+
768
830
  /**
769
831
  * Resume a completed agent's session with a new message.
770
832
  * Uses --resume SESSION_ID for zero cold-start continuation.