groove-dev 0.26.20 → 0.26.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@groove-dev/daemon/src/introducer.js +8 -2
- package/node_modules/@groove-dev/daemon/src/process.js +94 -32
- package/node_modules/@groove-dev/gui/dist/assets/{index-CBgUozrt.js → index-DOejqkiH.js} +23 -23
- package/node_modules/@groove-dev/gui/dist/assets/{index-CHRPn_ls.css → index-DomJ4Dgb.css} +1 -1
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/ui/thinking-indicator.jsx +35 -73
- package/node_modules/@groove-dev/gui/src/stores/groove.js +39 -14
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +49 -18
- package/package.json +1 -1
- package/packages/daemon/src/introducer.js +8 -2
- package/packages/daemon/src/process.js +94 -32
- package/packages/gui/dist/assets/{index-CBgUozrt.js → index-DOejqkiH.js} +23 -23
- package/packages/gui/dist/assets/{index-CHRPn_ls.css → index-DomJ4Dgb.css} +1 -1
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/components/agents/agent-feed.jsx +5 -5
- package/packages/gui/src/components/ui/thinking-indicator.jsx +35 -73
- package/packages/gui/src/stores/groove.js +39 -14
- package/packages/gui/src/views/agents.jsx +49 -18
|
@@ -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
|
|
5
|
-
'
|
|
6
|
-
'
|
|
7
|
-
'Planning approach',
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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 [
|
|
22
|
-
const [
|
|
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(() =>
|
|
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={
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
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
|
);
|
|
@@ -136,15 +136,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
136
136
|
case 'agent:output': {
|
|
137
137
|
const { agentId, data } = msg;
|
|
138
138
|
|
|
139
|
-
// Clear thinking indicator when agent responds
|
|
140
|
-
if (get().thinkingAgents.has(agentId)) {
|
|
141
|
-
set((s) => {
|
|
142
|
-
const next = new Set(s.thinkingAgents);
|
|
143
|
-
next.delete(agentId);
|
|
144
|
-
return { thinkingAgents: next };
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
139
|
// Separate text content from tool calls
|
|
149
140
|
let chatText = '';
|
|
150
141
|
let activityText = '';
|
|
@@ -177,6 +168,15 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
177
168
|
(data.type === 'activity' && typeof data.data === 'string')
|
|
178
169
|
);
|
|
179
170
|
if (showAsChat) {
|
|
171
|
+
// Clear thinking indicator only when actual text renders as a chat bubble
|
|
172
|
+
if (get().thinkingAgents.has(agentId)) {
|
|
173
|
+
set((s) => {
|
|
174
|
+
const next = new Set(s.thinkingAgents);
|
|
175
|
+
next.delete(agentId);
|
|
176
|
+
return { thinkingAgents: next };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
180
|
const trimmed = chatText.trim();
|
|
181
181
|
const history = { ...get().chatHistory };
|
|
182
182
|
if (!history[agentId]) history[agentId] = [];
|
|
@@ -516,21 +516,46 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
516
516
|
async checkRecommendedTeam() {
|
|
517
517
|
try {
|
|
518
518
|
const data = await api.get('/recommended-team');
|
|
519
|
-
if (data
|
|
520
|
-
set({ recommendedTeam: data });
|
|
521
|
-
} else {
|
|
519
|
+
if (!data || !data.agents?.length) {
|
|
522
520
|
set({ recommendedTeam: null });
|
|
521
|
+
return;
|
|
523
522
|
}
|
|
523
|
+
|
|
524
|
+
// Check if all recommended roles already exist in the planner's team.
|
|
525
|
+
// If so, auto-delegate instead of showing the "Launch Team" modal.
|
|
526
|
+
const planners = get().agents.filter((a) => a.role === 'planner');
|
|
527
|
+
const planner = planners.sort((a, b) => (b.lastActivity || '').localeCompare(a.lastActivity || ''))[0];
|
|
528
|
+
const teamId = planner?.teamId;
|
|
529
|
+
|
|
530
|
+
if (teamId) {
|
|
531
|
+
const teamAgents = get().agents.filter((a) => a.teamId === teamId && a.role !== 'planner');
|
|
532
|
+
const phase1Roles = data.agents.filter((a) => !a.phase || a.phase === 1).map((a) => a.role);
|
|
533
|
+
const allExist = phase1Roles.every((role) => teamAgents.some((a) => a.role === role));
|
|
534
|
+
|
|
535
|
+
if (allExist && phase1Roles.length > 0) {
|
|
536
|
+
// Auto-delegate — all agents already exist in the team
|
|
537
|
+
set({ recommendedTeam: null });
|
|
538
|
+
const result = await api.post('/recommended-team/launch');
|
|
539
|
+
const names = result.agents?.map((a) => a.name).join(', ') || '';
|
|
540
|
+
get().addToast('success', 'Planner delegated work', names ? `→ ${names}` : undefined);
|
|
541
|
+
api.post('/cleanup').catch(() => {});
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// New agents needed — show the modal for approval
|
|
547
|
+
set({ recommendedTeam: data });
|
|
524
548
|
} catch {
|
|
525
549
|
set({ recommendedTeam: null });
|
|
526
550
|
}
|
|
527
551
|
},
|
|
528
552
|
|
|
529
|
-
async launchRecommendedTeam() {
|
|
553
|
+
async launchRecommendedTeam(modifiedAgents) {
|
|
530
554
|
try {
|
|
531
555
|
set({ recommendedTeam: null }); // Dismiss modal immediately
|
|
532
556
|
get().addToast('info', 'Launching team...');
|
|
533
|
-
const
|
|
557
|
+
const body = modifiedAgents ? { agents: modifiedAgents } : undefined;
|
|
558
|
+
const result = await api.post('/recommended-team/launch', body);
|
|
534
559
|
const sub = [
|
|
535
560
|
result.phase2Pending ? `${result.phase2Pending} QC queued` : '',
|
|
536
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
|
-
|
|
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-
|
|
511
|
-
{/* Phase 1 agents */}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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.
|
|
3
|
+
"version": "0.26.22",
|
|
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)",
|
|
@@ -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
|
|
79
|
-
lines.push(`- If you
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
|
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, "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
+
4. NEVER create new agent names or custom roles. Use the standard roles: frontend, backend, fullstack.
|
|
130
134
|
|
|
131
|
-
|
|
135
|
+
5. NEVER instruct agents to delete files from other projects or clean up unrelated code.
|
|
132
136
|
|
|
133
|
-
|
|
137
|
+
6. You MUST always write .groove/recommended-team.json. NEVER skip it.
|
|
134
138
|
|
|
135
|
-
|
|
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
|
|
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.
|