groove-dev 0.27.145 → 0.27.146
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/CLAUDE.md +7 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +12 -6
- package/node_modules/@groove-dev/daemon/src/conversations.js +41 -10
- package/node_modules/@groove-dev/daemon/src/introducer.js +20 -0
- package/node_modules/@groove-dev/daemon/src/process.js +262 -15
- package/node_modules/@groove-dev/daemon/src/providers/groove-network.js +1 -3
- package/node_modules/@groove-dev/daemon/src/rotator.js +15 -3
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +43 -0
- package/node_modules/@groove-dev/daemon/templates/lab-general.json +12 -0
- package/node_modules/@groove-dev/daemon/templates/llama-cpp-setup.json +12 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BKbsE_hn.js +1011 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CEkPsSAm.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +132 -4
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +1 -8
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +135 -13
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +21 -4
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +6 -5
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +9 -3
- package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +13 -3
- package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +1 -3
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +24 -1
- package/node_modules/@groove-dev/gui/src/components/ui/question-modal.jsx +107 -0
- package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/stores/groove.js +32 -2
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +10 -1
- package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +1 -0
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +27 -22
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +12 -6
- package/packages/daemon/src/conversations.js +41 -10
- package/packages/daemon/src/introducer.js +20 -0
- package/packages/daemon/src/process.js +262 -15
- package/packages/daemon/src/providers/groove-network.js +1 -3
- package/packages/daemon/src/rotator.js +15 -3
- package/packages/daemon/src/routes/agents.js +43 -0
- package/packages/daemon/templates/lab-general.json +12 -0
- package/packages/daemon/templates/llama-cpp-setup.json +12 -0
- package/packages/gui/dist/assets/index-BKbsE_hn.js +1011 -0
- package/packages/gui/dist/assets/index-CEkPsSAm.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/spawn-wizard.jsx +132 -4
- package/packages/gui/src/components/chat/chat-header.jsx +1 -8
- package/packages/gui/src/components/chat/chat-input.jsx +135 -13
- package/packages/gui/src/components/chat/chat-messages.jsx +21 -4
- package/packages/gui/src/components/chat/chat-view.jsx +6 -5
- package/packages/gui/src/components/chat/model-picker.jsx +3 -3
- package/packages/gui/src/components/lab/chat-playground.jsx +3 -3
- package/packages/gui/src/components/lab/lab-assistant.jsx +9 -3
- package/packages/gui/src/components/lab/metrics-panel.jsx +13 -3
- package/packages/gui/src/components/lab/parameter-panel.jsx +5 -5
- package/packages/gui/src/components/lab/runtime-config.jsx +1 -3
- package/packages/gui/src/components/layout/app-shell.jsx +2 -0
- package/packages/gui/src/components/layout/status-bar.jsx +24 -1
- package/packages/gui/src/components/ui/question-modal.jsx +107 -0
- package/packages/gui/src/components/ui/sheet.jsx +2 -2
- package/packages/gui/src/stores/groove.js +32 -2
- package/packages/gui/src/stores/slices/agents-slice.js +10 -1
- package/packages/gui/src/stores/slices/chat-slice.js +1 -0
- package/packages/gui/src/views/model-lab.jsx +27 -22
- package/node_modules/@groove-dev/gui/dist/assets/index-Bxc0gU06.js +0 -1006
- package/node_modules/@groove-dev/gui/dist/assets/index-C0pztKBn.css +0 -1
- package/packages/gui/dist/assets/index-Bxc0gU06.js +0 -1006
- package/packages/gui/dist/assets/index-C0pztKBn.css +0 -1
package/CLAUDE.md
CHANGED
|
@@ -295,3 +295,10 @@ Audit-driven release. Multi-agent orchestration system with 7 coordination layer
|
|
|
295
295
|
- Dashboard: routing donut, cache panel, context health gauges
|
|
296
296
|
- Monitor/QC agent mode (stay active, loop)
|
|
297
297
|
- Distribution: demo video, HN launch, Twitter content
|
|
298
|
+
|
|
299
|
+
<!-- GROOVE:START -->
|
|
300
|
+
## GROOVE Orchestration (auto-injected)
|
|
301
|
+
Active agents: 0
|
|
302
|
+
See AGENTS_REGISTRY.md for full agent state.
|
|
303
|
+
**Memory policy:** GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.
|
|
304
|
+
<!-- GROOVE:END -->
|
|
@@ -11,7 +11,7 @@ import { hostname, networkInterfaces, homedir } from 'os';
|
|
|
11
11
|
import { StringDecoder } from 'string_decoder';
|
|
12
12
|
import { request as httpRequest } from 'http';
|
|
13
13
|
import { lookup as mimeLookup } from './mimetypes.js';
|
|
14
|
-
import { listProviders, getProvider, clearInstallCache, getProviderMetadata, getProviderPath, setProviderPaths } from './providers/index.js';
|
|
14
|
+
import { listProviders, getProvider, clearInstallCache, getProviderMetadata, getProviderPath, setProviderPaths, isProviderInstalled } from './providers/index.js';
|
|
15
15
|
import { OllamaProvider } from './providers/ollama.js';
|
|
16
16
|
import { ClaudeCodeProvider } from './providers/claude-code.js';
|
|
17
17
|
import { supportsSignalFlag, compareSemver, parseSemver } from './providers/groove-network.js';
|
|
@@ -1336,7 +1336,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1336
1336
|
|
|
1337
1337
|
app.post('/api/onboarding/set-default', async (req, res) => {
|
|
1338
1338
|
const { provider, model } = req.body;
|
|
1339
|
-
const validProviders = ['claude-code', 'codex', 'gemini', 'ollama'];
|
|
1339
|
+
const validProviders = ['claude-code', 'codex', 'gemini', 'grok', 'ollama', 'local'];
|
|
1340
1340
|
if (!provider || !validProviders.includes(provider)) {
|
|
1341
1341
|
return res.status(400).json({ error: `Invalid provider. Valid: ${validProviders.join(', ')}` });
|
|
1342
1342
|
}
|
|
@@ -1721,8 +1721,9 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1721
1721
|
app.post('/api/lab/assistant', async (req, res) => {
|
|
1722
1722
|
try {
|
|
1723
1723
|
const { backend, model } = req.body || {};
|
|
1724
|
-
|
|
1725
|
-
|
|
1724
|
+
const validBackends = ['vllm', 'tgi', 'mlx', 'llama-cpp', 'lab-general'];
|
|
1725
|
+
if (!backend || !validBackends.includes(backend)) {
|
|
1726
|
+
return res.status(400).json({ error: `backend must be one of: ${validBackends.join(', ')}` });
|
|
1726
1727
|
}
|
|
1727
1728
|
const templatePath = resolve(__dirname, `../templates/${backend}-setup.json`);
|
|
1728
1729
|
const template = JSON.parse(readFileSync(templatePath, 'utf8'));
|
|
@@ -1733,14 +1734,19 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1733
1734
|
const desc = parts.join(', ');
|
|
1734
1735
|
prompt = `The user has selected a local model: ${desc} (id: ${model.id}).\nUse this model for setup instead of recommending a different one. If this exact model isn't available in the runtime's format, find the closest equivalent (same base model, similar quantization).\n\n${prompt}`;
|
|
1735
1736
|
}
|
|
1737
|
+
// Pick best available CLI provider: prefer user's default, fall back through tool-use capable providers
|
|
1738
|
+
const cliProviders = ['claude-code', 'codex', 'gemini'];
|
|
1739
|
+
const defaultProv = daemon.config.defaultProvider;
|
|
1740
|
+
let assistantProvider = cliProviders.includes(defaultProv) && isProviderInstalled(defaultProv)
|
|
1741
|
+
? defaultProv
|
|
1742
|
+
: cliProviders.find((p) => isProviderInstalled(p)) || 'claude-code';
|
|
1736
1743
|
const config = {
|
|
1737
1744
|
role: 'lab-assistant',
|
|
1738
1745
|
scope: agentConfig.scope || [],
|
|
1739
|
-
provider:
|
|
1746
|
+
provider: assistantProvider,
|
|
1740
1747
|
prompt,
|
|
1741
1748
|
metadata: { labAssistant: true, backend },
|
|
1742
1749
|
};
|
|
1743
|
-
if (!config.provider) config.provider = daemon.config.defaultProvider;
|
|
1744
1750
|
const agent = await daemon.processes.spawn(config);
|
|
1745
1751
|
daemon.audit.log('lab.assistant.spawn', { id: agent.id, backend });
|
|
1746
1752
|
res.status(201).json({ agentId: agent.id, backend });
|
|
@@ -475,31 +475,62 @@ export class ConversationManager {
|
|
|
475
475
|
|
|
476
476
|
try {
|
|
477
477
|
const json = JSON.parse(trimmed);
|
|
478
|
+
let matched = false;
|
|
478
479
|
if (json.type === 'assistant' && json.message?.content) {
|
|
479
480
|
for (const block of json.message.content) {
|
|
480
481
|
if (block.type === 'text' && block.text) {
|
|
481
482
|
emitChunk(block.text);
|
|
482
483
|
}
|
|
483
484
|
}
|
|
484
|
-
|
|
485
|
+
matched = true;
|
|
485
486
|
}
|
|
486
|
-
if (json.type === 'content_block_delta' && json.delta?.text) {
|
|
487
|
+
if (!matched && json.type === 'content_block_delta' && json.delta?.text) {
|
|
487
488
|
emitChunk(json.delta.text);
|
|
488
|
-
|
|
489
|
+
matched = true;
|
|
489
490
|
}
|
|
490
|
-
if (json.type === 'result' && json.result)
|
|
491
|
-
|
|
491
|
+
if (!matched && json.type === 'result' && json.result) {
|
|
492
|
+
matched = true;
|
|
493
|
+
}
|
|
494
|
+
if (!matched && json.type === 'token' && json.text != null) {
|
|
492
495
|
emitChunk(json.text);
|
|
493
|
-
|
|
496
|
+
matched = true;
|
|
494
497
|
}
|
|
495
|
-
if ((json.type === 'done' || json.type === 'complete' || json.type === 'result') && json.text) {
|
|
498
|
+
if (!matched && (json.type === 'done' || json.type === 'complete' || json.type === 'result') && json.text) {
|
|
496
499
|
emitChunk(json.text);
|
|
497
|
-
|
|
500
|
+
matched = true;
|
|
498
501
|
}
|
|
499
|
-
if (json.content?.[0]?.text) {
|
|
502
|
+
if (!matched && json.content?.[0]?.text) {
|
|
500
503
|
emitChunk(json.content[0].text);
|
|
501
|
-
|
|
504
|
+
matched = true;
|
|
505
|
+
}
|
|
506
|
+
// Fallback: use provider's parseOutput for provider-specific formats (Gemini tool_use, tool_result, Codex items, etc.)
|
|
507
|
+
if (!matched && provider.parseOutput) {
|
|
508
|
+
const parsed = provider.parseOutput(trimmed);
|
|
509
|
+
if (parsed) {
|
|
510
|
+
const blocks = Array.isArray(parsed.data) ? parsed.data : [];
|
|
511
|
+
for (const block of blocks) {
|
|
512
|
+
if (block.type === 'text' && block.text) {
|
|
513
|
+
emitChunk(block.text);
|
|
514
|
+
} else if (block.type === 'tool_use') {
|
|
515
|
+
const cmd = block.input?.command;
|
|
516
|
+
const path = block.input?.path;
|
|
517
|
+
const summary = cmd || path || (block.input && Object.keys(block.input).length > 0
|
|
518
|
+
? Object.values(block.input)[0]
|
|
519
|
+
: null);
|
|
520
|
+
if (block.name || summary) {
|
|
521
|
+
this.daemon.broadcast({
|
|
522
|
+
type: 'conversation:tool',
|
|
523
|
+
data: { conversationId: id, name: block.name || 'Tool', summary: summary ? String(summary).slice(0, 120) : null },
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (!blocks.length && typeof parsed.data === 'string' && parsed.data) {
|
|
529
|
+
emitChunk(parsed.data);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
502
532
|
}
|
|
533
|
+
continue;
|
|
503
534
|
} catch { /* not JSON */ }
|
|
504
535
|
|
|
505
536
|
if (!trimmed.startsWith('{')) {
|
|
@@ -106,6 +106,26 @@ export class Introducer {
|
|
|
106
106
|
|
|
107
107
|
if (others.length === 0) {
|
|
108
108
|
lines.push('You are the only agent on this project right now.');
|
|
109
|
+
|
|
110
|
+
// Solo agents get full authority — no team coordination, no scope limits.
|
|
111
|
+
// Business roles and planners keep their restrictions (intentional by design).
|
|
112
|
+
const NO_SOLO_EXPAND = new Set([
|
|
113
|
+
'planner', 'cmo', 'cfo', 'ea', 'support', 'analyst', 'home', 'chat', 'ambassador',
|
|
114
|
+
]);
|
|
115
|
+
if (!NO_SOLO_EXPAND.has(newAgent.role)) {
|
|
116
|
+
lines.push('');
|
|
117
|
+
lines.push('## Solo Mode');
|
|
118
|
+
lines.push('');
|
|
119
|
+
lines.push('You are working alone — no team, no scope restrictions, no coordination needed. You have full authority to do whatever the task requires:');
|
|
120
|
+
lines.push('- Install dependencies (npm install, pip install, etc.)');
|
|
121
|
+
lines.push('- Start dev servers and long-running processes when needed');
|
|
122
|
+
lines.push('- Run tests, builds, and linters');
|
|
123
|
+
lines.push('- Create, modify, or delete any project files');
|
|
124
|
+
lines.push('- Commit and manage git operations');
|
|
125
|
+
lines.push('- Perform any shell commands necessary to complete your task');
|
|
126
|
+
lines.push('');
|
|
127
|
+
lines.push('You are not limited to your role\'s typical focus area. If the task requires work outside your specialty, handle it directly.');
|
|
128
|
+
}
|
|
109
129
|
} else {
|
|
110
130
|
lines.push(`## Team (${others.length} other agent${others.length > 1 ? 's' : ''})`);
|
|
111
131
|
lines.push('');
|
|
@@ -345,6 +345,10 @@ export class ProcessManager {
|
|
|
345
345
|
this._truncationFlagged = new Set(); // agentIds that have had any truncation in their session
|
|
346
346
|
this._lastAssistantBlocks = new Map(); // agentId -> last assistant content blocks (for abandoned tool_use detection)
|
|
347
347
|
this._previousCacheReadTokens = new Map(); // agentId -> previous turn's cacheReadTokens
|
|
348
|
+
this.pendingQuestions = new Map(); // agentId -> { id, agentId, agentName, questions, timestamp }
|
|
349
|
+
this._reviewTriggered = new Set(); // teamIds that have had review triggered (one round only)
|
|
350
|
+
this._pendingReviews = new Map(); // teamId -> { reviewAgentId }
|
|
351
|
+
this._reviewPending = new Set(); // teamIds with review in progress (blocks preview launch)
|
|
348
352
|
|
|
349
353
|
this._stallWatchdog = setInterval(() => this._checkStalls(), STALL_CHECK_INTERVAL_MS);
|
|
350
354
|
if (this._stallWatchdog.unref) this._stallWatchdog.unref();
|
|
@@ -503,6 +507,7 @@ export class ProcessManager {
|
|
|
503
507
|
|
|
504
508
|
if (finalStatus === 'completed' && agent.role === 'planner') {
|
|
505
509
|
this._extractRecommendedTeam(agent, logPath);
|
|
510
|
+
this._handleReviewComplete(agent);
|
|
506
511
|
}
|
|
507
512
|
|
|
508
513
|
if (finalStatus === 'completed') {
|
|
@@ -529,6 +534,10 @@ export class ProcessManager {
|
|
|
529
534
|
|
|
530
535
|
this._checkPhase2(agent.id);
|
|
531
536
|
|
|
537
|
+
if (finalStatus === 'completed' && agent.role === 'fullstack' && agent.teamId) {
|
|
538
|
+
this._triggerReview(agent);
|
|
539
|
+
}
|
|
540
|
+
|
|
532
541
|
if (agent.teamId) {
|
|
533
542
|
this._checkPreviewReady(agent.teamId);
|
|
534
543
|
}
|
|
@@ -593,6 +602,20 @@ export class ProcessManager {
|
|
|
593
602
|
const hadResult = this._resultReceived.has(agent.id);
|
|
594
603
|
this._resultReceived.delete(agent.id);
|
|
595
604
|
|
|
605
|
+
// Agent exited while waiting for user answer — keep it in "waiting" state
|
|
606
|
+
// instead of marking completed. The GUI will relay the user's answer and
|
|
607
|
+
// trigger a resume via POST /api/agents/:id/answer.
|
|
608
|
+
const pendingQ = this.pendingQuestions.get(agent.id);
|
|
609
|
+
if (pendingQ && code === 0) {
|
|
610
|
+
registry.update(agent.id, { status: 'waiting_for_input', pid: null });
|
|
611
|
+
this.daemon.broadcast({
|
|
612
|
+
type: 'agent:exit',
|
|
613
|
+
agentId: agent.id, code, signal,
|
|
614
|
+
status: 'waiting_for_input',
|
|
615
|
+
});
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
596
619
|
const finalStatus = hadResult ? 'completed' : signal === 'SIGTERM' || signal === 'SIGKILL' ? 'killed' : code === 0 ? 'completed' : 'crashed';
|
|
597
620
|
registry.update(agent.id, { status: finalStatus, pid: null });
|
|
598
621
|
|
|
@@ -779,21 +802,58 @@ export class ProcessManager {
|
|
|
779
802
|
);
|
|
780
803
|
}
|
|
781
804
|
|
|
782
|
-
// Pre-flight for local model providers: ensure
|
|
805
|
+
// Pre-flight for local model providers: ensure runtime/server is running
|
|
783
806
|
if (providerName === 'local' || providerName === 'ollama') {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
807
|
+
// Auto-start lab runtime when model is runtime:<runtimeId>:<modelId>
|
|
808
|
+
if (config.model && config.model.startsWith('runtime:') && this.daemon.modelLab) {
|
|
809
|
+
const parts = config.model.split(':');
|
|
810
|
+
const runtimeId = parts[1];
|
|
811
|
+
const rt = this.daemon.modelLab.getRuntime(runtimeId);
|
|
812
|
+
if (rt) {
|
|
813
|
+
const status = await this.daemon.modelLab.getRuntimeStatus(rt);
|
|
814
|
+
if (!status.online) {
|
|
815
|
+
console.log(`[Groove] Auto-starting runtime '${rt.name}' for agent spawn`);
|
|
816
|
+
try {
|
|
817
|
+
await this.daemon.modelLab.startRuntime(runtimeId);
|
|
818
|
+
} catch (err) {
|
|
819
|
+
throw new Error(`Failed to auto-start runtime '${rt.name}': ${err.message}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
} else if (config.model && config.model.startsWith('gguf:') && this.daemon.modelLab) {
|
|
824
|
+
// GGUF models are served by llama-cpp runtimes, not Ollama
|
|
825
|
+
const ggufId = config.model.slice(5);
|
|
826
|
+
const runtimes = this.daemon.modelLab.listRuntimes();
|
|
827
|
+
const rt = runtimes.find(r =>
|
|
828
|
+
r._localModelId === ggufId ||
|
|
829
|
+
r.models?.some(rm => rm.id === ggufId || rm.name === ggufId)
|
|
830
|
+
);
|
|
831
|
+
if (rt) {
|
|
832
|
+
const status = await this.daemon.modelLab.getRuntimeStatus(rt);
|
|
833
|
+
if (!status.online) {
|
|
834
|
+
console.log(`[Groove] Auto-starting runtime '${rt.name}' for GGUF model '${ggufId}'`);
|
|
835
|
+
try {
|
|
836
|
+
await this.daemon.modelLab.startRuntime(rt.id);
|
|
837
|
+
} catch (err) {
|
|
838
|
+
throw new Error(`Failed to auto-start runtime '${rt.name}': ${err.message}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
} else {
|
|
843
|
+
try {
|
|
844
|
+
await LocalProvider.ensureServerRunning();
|
|
845
|
+
} catch (err) {
|
|
846
|
+
const agent = registry.add({ ...config, provider: providerName, status: 'error' });
|
|
847
|
+
registry.update(agent.id, { status: 'error', error: 'Ollama server failed to start' });
|
|
848
|
+
this.daemon.broadcast({ type: 'model:error', agentId: agent.id, error: err.message });
|
|
849
|
+
throw new Error('Ollama server failed to start: ' + err.message);
|
|
850
|
+
}
|
|
851
|
+
if (config.model && config.model !== 'auto') {
|
|
852
|
+
const installed = OllamaProvider.getInstalledModels();
|
|
853
|
+
const modelInstalled = installed.some((m) => m.id === config.model || config.model.startsWith(m.id.split(':')[0]));
|
|
854
|
+
if (!modelInstalled) {
|
|
855
|
+
throw new Error(`Model '${config.model}' is not installed. Pull it first with: ollama pull ${config.model}`);
|
|
856
|
+
}
|
|
797
857
|
}
|
|
798
858
|
}
|
|
799
859
|
}
|
|
@@ -953,7 +1013,7 @@ export class ProcessManager {
|
|
|
953
1013
|
if (!isOneShotProvider) {
|
|
954
1014
|
// Apply role-specific prompt prefix so agents always get their role constraints
|
|
955
1015
|
const rolePrompt = ROLE_PROMPTS[agent.role];
|
|
956
|
-
if (rolePrompt) {
|
|
1016
|
+
if (rolePrompt && !config._skipRolePrompt) {
|
|
957
1017
|
if (!spawnConfig.prompt) {
|
|
958
1018
|
spawnConfig.prompt = rolePrompt + `IMPORTANT: No task has been assigned yet. You MUST wait for the user to tell you what to do.
|
|
959
1019
|
|
|
@@ -975,6 +1035,11 @@ DO: Introduce yourself in one sentence and ask the user what they would like you
|
|
|
975
1035
|
IMPORTANT: No task has been assigned yet. You MUST wait for the user to tell you what to do. Do NOT start building, coding, or continuing previous work. Do NOT treat existing files or the project map as your task. Introduce yourself in one sentence and ask the user what they would like you to work on. Then wait.`;
|
|
976
1036
|
}
|
|
977
1037
|
|
|
1038
|
+
// AskUserQuestion is unavailable in headless mode — instruct agents to use text output instead
|
|
1039
|
+
if (spawnConfig.prompt) {
|
|
1040
|
+
spawnConfig.prompt += '\n\nIMPORTANT: Do NOT use the AskUserQuestion tool. If you need user input or clarification, state your questions as regular text output — the user will see your output and respond via the chat interface.';
|
|
1041
|
+
}
|
|
1042
|
+
|
|
978
1043
|
// Inject skill content into the prompt
|
|
979
1044
|
if (config.skills?.length > 0 && this.daemon.skills) {
|
|
980
1045
|
const skillSections = [];
|
|
@@ -1405,6 +1470,23 @@ For normal file edits within your scope, proceed without review.
|
|
|
1405
1470
|
|
|
1406
1471
|
this._lastAssistantBlocks.set(agentId, blocks);
|
|
1407
1472
|
|
|
1473
|
+
// Detect AskUserQuestion tool calls — these cause the agent to exit
|
|
1474
|
+
// in headless mode because there's no terminal to collect input.
|
|
1475
|
+
// Intercept and relay to the GUI instead.
|
|
1476
|
+
const askBlock = blocks.find(b => b.type === 'tool_use' && b.name === 'AskUserQuestion');
|
|
1477
|
+
if (askBlock) {
|
|
1478
|
+
const questionData = {
|
|
1479
|
+
id: askBlock.id || `q_${Date.now()}`,
|
|
1480
|
+
agentId,
|
|
1481
|
+
agentName: agent.name,
|
|
1482
|
+
questions: askBlock.input?.questions || [{ question: askBlock.input?.question || JSON.stringify(askBlock.input || {}).slice(0, 500) }],
|
|
1483
|
+
timestamp: Date.now(),
|
|
1484
|
+
};
|
|
1485
|
+
this.pendingQuestions.set(agentId, questionData);
|
|
1486
|
+
registry.update(agentId, { pendingQuestion: true });
|
|
1487
|
+
this.daemon.broadcast({ type: 'agent:question', agentId, data: questionData });
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1408
1490
|
if (truncated) {
|
|
1409
1491
|
this._truncationFlagged.add(agentId);
|
|
1410
1492
|
const prev = agent.consecutiveTruncations || 0;
|
|
@@ -1538,6 +1620,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
1538
1620
|
* with _previewAttempted per teamId.
|
|
1539
1621
|
*/
|
|
1540
1622
|
_checkPreviewReady(teamId) {
|
|
1623
|
+
if (this._reviewPending?.has(teamId)) return;
|
|
1541
1624
|
const preview = this.daemon.preview;
|
|
1542
1625
|
if (!preview) return;
|
|
1543
1626
|
if (!this._previewAttempted) this._previewAttempted = new Set();
|
|
@@ -1788,6 +1871,170 @@ For normal file edits within your scope, proceed without review.
|
|
|
1788
1871
|
}
|
|
1789
1872
|
}
|
|
1790
1873
|
|
|
1874
|
+
/**
|
|
1875
|
+
* After the phase-2 fullstack QC completes in a planned team build,
|
|
1876
|
+
* auto-spawn a review planner that checks the implementation against
|
|
1877
|
+
* the original spec. One round only — _reviewTriggered guards re-entry.
|
|
1878
|
+
*/
|
|
1879
|
+
_triggerReview(agent) {
|
|
1880
|
+
const teamId = agent.teamId;
|
|
1881
|
+
if (!teamId) return;
|
|
1882
|
+
if (this._reviewTriggered.has(teamId)) return;
|
|
1883
|
+
|
|
1884
|
+
const registry = this.daemon.registry;
|
|
1885
|
+
const teamAgents = registry.getAll().filter(a => a.teamId === teamId);
|
|
1886
|
+
|
|
1887
|
+
// Only trigger for planned team builds — must have a completed planner
|
|
1888
|
+
const planner = teamAgents.find(a => a.role === 'planner' &&
|
|
1889
|
+
(a.status === 'completed' || a.status === 'crashed' || a.status === 'stopped' || a.status === 'killed'));
|
|
1890
|
+
if (!planner) return;
|
|
1891
|
+
|
|
1892
|
+
// All non-planner agents must be done
|
|
1893
|
+
const hasRunning = teamAgents.some(a =>
|
|
1894
|
+
a.role !== 'planner' && (a.status === 'running' || a.status === 'starting'));
|
|
1895
|
+
if (hasRunning) return;
|
|
1896
|
+
|
|
1897
|
+
this._reviewTriggered.add(teamId);
|
|
1898
|
+
this._reviewPending.add(teamId);
|
|
1899
|
+
|
|
1900
|
+
const journalist = this.daemon.journalist;
|
|
1901
|
+
const originalSpec = planner.prompt || '';
|
|
1902
|
+
const plannerResult = journalist?.getAgentResult(planner) || '';
|
|
1903
|
+
|
|
1904
|
+
// Collect all files modified by non-planner team agents
|
|
1905
|
+
const allFiles = new Set();
|
|
1906
|
+
for (const a of teamAgents) {
|
|
1907
|
+
if (a.role === 'planner') continue;
|
|
1908
|
+
for (const f of (journalist?.getAgentFiles(a) || [])) allFiles.add(f);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
const reviewPrompt = `You are reviewing a completed team build against the original specification.
|
|
1912
|
+
|
|
1913
|
+
## Original Task
|
|
1914
|
+
${originalSpec.slice(0, 2000)}
|
|
1915
|
+
|
|
1916
|
+
## Plan
|
|
1917
|
+
${plannerResult.slice(0, 3000)}
|
|
1918
|
+
|
|
1919
|
+
## Files Created/Modified
|
|
1920
|
+
${[...allFiles].join('\n')}
|
|
1921
|
+
|
|
1922
|
+
Read the key files listed above and check:
|
|
1923
|
+
1. Are all features from the spec implemented?
|
|
1924
|
+
2. Are there bugs, missing error handling, or broken functionality?
|
|
1925
|
+
3. Does the implementation match the architectural decisions in the plan?
|
|
1926
|
+
|
|
1927
|
+
Output a JSON block with your verdict:
|
|
1928
|
+
\`\`\`json
|
|
1929
|
+
{ "pass": true, "issues": [] }
|
|
1930
|
+
\`\`\`
|
|
1931
|
+
|
|
1932
|
+
Or if issues are found:
|
|
1933
|
+
\`\`\`json
|
|
1934
|
+
{
|
|
1935
|
+
"pass": false,
|
|
1936
|
+
"issues": [
|
|
1937
|
+
{ "file": "path/to/file", "problem": "description", "fix": "what to change" }
|
|
1938
|
+
]
|
|
1939
|
+
}
|
|
1940
|
+
\`\`\`
|
|
1941
|
+
|
|
1942
|
+
If everything matches the spec, set pass: true with empty issues.
|
|
1943
|
+
Do NOT write .groove/recommended-team.json. Do NOT plan a new team. Just review and report.`;
|
|
1944
|
+
|
|
1945
|
+
const config = validateAgentConfig({
|
|
1946
|
+
role: 'planner',
|
|
1947
|
+
prompt: reviewPrompt,
|
|
1948
|
+
workingDir: agent.workingDir,
|
|
1949
|
+
teamId,
|
|
1950
|
+
scope: [],
|
|
1951
|
+
});
|
|
1952
|
+
config._skipRolePrompt = true;
|
|
1953
|
+
|
|
1954
|
+
this.spawn(config).then(reviewAgent => {
|
|
1955
|
+
this._pendingReviews.set(teamId, { reviewAgentId: reviewAgent.id });
|
|
1956
|
+
this.daemon.audit?.log('review.started', { teamId, reviewAgentId: reviewAgent.id });
|
|
1957
|
+
this.daemon.broadcast({ type: 'review:started', teamId, agentId: reviewAgent.id, name: reviewAgent.name });
|
|
1958
|
+
console.log(`[Groove] Review planner ${reviewAgent.name} spawned for team ${teamId}`);
|
|
1959
|
+
}).catch(err => {
|
|
1960
|
+
console.error(`[Groove] Review spawn failed: ${err.message}`);
|
|
1961
|
+
this._reviewPending.delete(teamId);
|
|
1962
|
+
this._pendingReviews.delete(teamId);
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
/**
|
|
1967
|
+
* When a review planner completes, extract its verdict.
|
|
1968
|
+
* If issues found, spawn a fullstack to fix them. If clean, let preview proceed.
|
|
1969
|
+
*/
|
|
1970
|
+
_handleReviewComplete(agent) {
|
|
1971
|
+
const teamId = agent.teamId;
|
|
1972
|
+
if (!teamId) return;
|
|
1973
|
+
|
|
1974
|
+
const pending = this._pendingReviews.get(teamId);
|
|
1975
|
+
if (!pending || pending.reviewAgentId !== agent.id) return;
|
|
1976
|
+
this._pendingReviews.delete(teamId);
|
|
1977
|
+
|
|
1978
|
+
const journalist = this.daemon.journalist;
|
|
1979
|
+
const report = journalist?.getAgentResult(agent) || '';
|
|
1980
|
+
|
|
1981
|
+
// Try to parse the structured verdict from the report
|
|
1982
|
+
let pass = true;
|
|
1983
|
+
let issues = [];
|
|
1984
|
+
const jsonMatch = report.match(/```json\s*([\s\S]*?)```/) || report.match(/\{[\s\S]*?"pass"\s*:[\s\S]*?\}/);
|
|
1985
|
+
if (jsonMatch) {
|
|
1986
|
+
try {
|
|
1987
|
+
const parsed = JSON.parse(jsonMatch[1] || jsonMatch[0]);
|
|
1988
|
+
pass = parsed.pass === true;
|
|
1989
|
+
issues = Array.isArray(parsed.issues) ? parsed.issues : [];
|
|
1990
|
+
} catch { /* fall through to text analysis */ }
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// Fallback: if no JSON parsed, check for pass/fail signals in text
|
|
1994
|
+
if (!jsonMatch) {
|
|
1995
|
+
const lower = report.toLowerCase();
|
|
1996
|
+
pass = lower.includes('pass') && !lower.includes('fail') && !lower.includes('issue');
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
if (pass || issues.length === 0) {
|
|
2000
|
+
this._reviewPending.delete(teamId);
|
|
2001
|
+
this.daemon.audit?.log('review.passed', { teamId });
|
|
2002
|
+
this.daemon.broadcast({ type: 'review:passed', teamId });
|
|
2003
|
+
console.log(`[Groove] Review passed for team ${teamId}`);
|
|
2004
|
+
if (agent.teamId) this._checkPreviewReady(agent.teamId);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// Build fix prompt from issues
|
|
2009
|
+
const issuesList = issues.map((iss, i) =>
|
|
2010
|
+
`${i + 1}. **${iss.file || 'unknown'}**: ${iss.problem || ''}${iss.fix ? `\n Fix: ${iss.fix}` : ''}`
|
|
2011
|
+
).join('\n');
|
|
2012
|
+
|
|
2013
|
+
const fixPrompt = `The QC review found the following issues. Fix each one:
|
|
2014
|
+
|
|
2015
|
+
${issuesList}
|
|
2016
|
+
|
|
2017
|
+
After fixing all issues, run tests (npm test) and build (npm run build) to verify everything works.`;
|
|
2018
|
+
|
|
2019
|
+
const fixConfig = validateAgentConfig({
|
|
2020
|
+
role: 'fullstack',
|
|
2021
|
+
prompt: fixPrompt,
|
|
2022
|
+
workingDir: agent.workingDir,
|
|
2023
|
+
teamId,
|
|
2024
|
+
scope: [],
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
this.spawn(fixConfig).then(fixAgent => {
|
|
2028
|
+
this._reviewPending.delete(teamId);
|
|
2029
|
+
this.daemon.audit?.log('review.fixStarted', { teamId, fixAgentId: fixAgent.id, issueCount: issues.length });
|
|
2030
|
+
this.daemon.broadcast({ type: 'review:fix-started', teamId, agentId: fixAgent.id, name: fixAgent.name, issueCount: issues.length });
|
|
2031
|
+
console.log(`[Groove] Fix fullstack ${fixAgent.name} spawned with ${issues.length} issues for team ${teamId}`);
|
|
2032
|
+
}).catch(err => {
|
|
2033
|
+
console.error(`[Groove] Review fix spawn failed: ${err.message}`);
|
|
2034
|
+
this._reviewPending.delete(teamId);
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
|
|
1791
2038
|
/**
|
|
1792
2039
|
* Auto-trigger an idle QC agent in the same team when a teammate completes real work.
|
|
1793
2040
|
* "Idle" = running fullstack agent that hasn't modified any files yet.
|
|
@@ -78,9 +78,7 @@ export class GrooveNetworkProvider extends Provider {
|
|
|
78
78
|
static authType = 'none';
|
|
79
79
|
static isOneShot = true;
|
|
80
80
|
|
|
81
|
-
static models = [
|
|
82
|
-
{ id: 'Qwen/Qwen3-4B', name: 'Qwen 3 4B (Network)', context: 32768 },
|
|
83
|
-
];
|
|
81
|
+
static models = [];
|
|
84
82
|
|
|
85
83
|
static isInstalled() {
|
|
86
84
|
const cfg = getConfig();
|
|
@@ -60,11 +60,23 @@ export class Rotator extends EventEmitter {
|
|
|
60
60
|
const rotateEvents = events.filter((e) => e.type === 'rotate');
|
|
61
61
|
if (rotateEvents.length === 0) return;
|
|
62
62
|
|
|
63
|
-
const
|
|
63
|
+
const existingEntries = this.rotationHistory.map((r) => ({
|
|
64
|
+
agentId: r.agentId,
|
|
65
|
+
reason: r.reason,
|
|
66
|
+
ts: new Date(r.timestamp).getTime(),
|
|
67
|
+
}));
|
|
64
68
|
let added = 0;
|
|
65
69
|
for (const e of rotateEvents) {
|
|
66
|
-
const
|
|
67
|
-
|
|
70
|
+
const eventTs = typeof e.t === 'number' ? e.t : new Date(e.t).getTime();
|
|
71
|
+
const eventAgentId = e.oldAgentId || e.agentId;
|
|
72
|
+
const eventReason = e.reason || 'context_threshold';
|
|
73
|
+
const isDupe = existingEntries.some((existing) =>
|
|
74
|
+
existing.agentId === eventAgentId &&
|
|
75
|
+
existing.reason === eventReason &&
|
|
76
|
+
Math.abs(existing.ts - eventTs) < 10_000
|
|
77
|
+
);
|
|
78
|
+
if (isDupe) continue;
|
|
79
|
+
const ts = new Date(eventTs).toISOString();
|
|
68
80
|
this.rotationHistory.push({
|
|
69
81
|
agentId: e.oldAgentId || e.agentId,
|
|
70
82
|
agentName: e.agentName || 'unknown',
|
|
@@ -12,6 +12,16 @@ export function registerAgentRoutes(app, daemon) {
|
|
|
12
12
|
res.json(daemon.registry.getAll());
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
// List all pending questions from agents awaiting user input
|
|
16
|
+
// (registered before :id route so Express doesn't capture "questions" as an id)
|
|
17
|
+
app.get('/api/agents/questions', (req, res) => {
|
|
18
|
+
const questions = [];
|
|
19
|
+
for (const [, q] of daemon.processes.pendingQuestions) {
|
|
20
|
+
questions.push(q);
|
|
21
|
+
}
|
|
22
|
+
res.json(questions);
|
|
23
|
+
});
|
|
24
|
+
|
|
15
25
|
// Get single agent
|
|
16
26
|
app.get('/api/agents/:id', (req, res) => {
|
|
17
27
|
const agent = daemon.registry.get(req.params.id);
|
|
@@ -506,6 +516,39 @@ export function registerAgentRoutes(app, daemon) {
|
|
|
506
516
|
}
|
|
507
517
|
});
|
|
508
518
|
|
|
519
|
+
// Answer a pending question from an agent
|
|
520
|
+
app.post('/api/agents/:id/answer', async (req, res) => {
|
|
521
|
+
try {
|
|
522
|
+
const { answers } = req.body;
|
|
523
|
+
if (!answers || typeof answers !== 'object') {
|
|
524
|
+
return res.status(400).json({ error: 'answers object is required' });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const agent = daemon.registry.get(req.params.id);
|
|
528
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
529
|
+
|
|
530
|
+
const pending = daemon.processes.pendingQuestions.get(req.params.id);
|
|
531
|
+
if (!pending) return res.status(404).json({ error: 'No pending question for this agent' });
|
|
532
|
+
|
|
533
|
+
daemon.processes.pendingQuestions.delete(req.params.id);
|
|
534
|
+
daemon.registry.update(req.params.id, { pendingQuestion: false });
|
|
535
|
+
daemon.broadcast({ type: 'agent:question:resolved', agentId: req.params.id });
|
|
536
|
+
|
|
537
|
+
// Format the answers as a natural language message for session resume
|
|
538
|
+
const parts = Object.entries(answers).map(([q, a]) => `Q: ${q}\nA: ${a}`);
|
|
539
|
+
const resumeMessage = `The user answered your questions:\n\n${parts.join('\n\n')}\n\nContinue with your task based on these answers.`;
|
|
540
|
+
|
|
541
|
+
const newAgent = agent.sessionId
|
|
542
|
+
? await daemon.processes.resume(req.params.id, resumeMessage)
|
|
543
|
+
: await daemon.rotator.rotate(req.params.id, { additionalPrompt: resumeMessage });
|
|
544
|
+
|
|
545
|
+
daemon.audit.log('agent.answer', { id: req.params.id, newId: newAgent.id });
|
|
546
|
+
res.json(newAgent);
|
|
547
|
+
} catch (err) {
|
|
548
|
+
res.status(400).json({ error: err.message });
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
509
552
|
// Query an agent (headless one-shot, agent keeps running)
|
|
510
553
|
// For agent loop agents: sends message directly to the loop
|
|
511
554
|
app.post('/api/agents/:id/query', async (req, res) => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lab-general",
|
|
3
|
+
"description": "General-purpose Lab Assistant for runtime management, model configuration, and optimization",
|
|
4
|
+
"agents": [
|
|
5
|
+
{
|
|
6
|
+
"role": "lab-assistant",
|
|
7
|
+
"scope": [],
|
|
8
|
+
"provider": "claude-code",
|
|
9
|
+
"prompt": "You are a GROOVE Lab Assistant — a general-purpose helper for local AI model inference. You help with runtime setup, model configuration, optimization, and troubleshooting.\n\nYou can help with:\n- Setting up inference runtimes (MLX, llama.cpp, vLLM, TGI, Ollama)\n- Model selection and downloading\n- Context window configuration and memory optimization\n- Prompt engineering and system prompt design\n- Performance tuning (quantization, batch size, GPU layers)\n- Troubleshooting server issues\n- Comparing model performance\n\n## System Info\n\nStart by understanding the user's environment if needed:\n```bash\nuname -m && sysctl -n machdep.cpu.brand_string 2>/dev/null; sysctl -n hw.memsize 2>/dev/null | awk '{print $0/1073741824 \" GB\"}'; python3 --version 2>/dev/null\n```\n\n## Runtime Management\n\nCheck existing runtimes:\n```bash\nDAEMON_PORT=$(cat ~/.groove/daemon.port 2>/dev/null || echo 31415)\ncurl -s http://localhost:$DAEMON_PORT/api/lab/runtimes | python3 -m json.tool 2>/dev/null || curl -s http://localhost:$DAEMON_PORT/api/lab/runtimes\n```\n\n## Registering Runtimes\n\nWhen setting up a new runtime, always register it with GROOVE so it can be managed from the UI:\n```bash\nDAEMON_PORT=$(cat ~/.groove/daemon.port 2>/dev/null || echo 31415)\ncurl -s -X POST http://localhost:$DAEMON_PORT/api/lab/runtimes \\\n -H 'Content-Type: application/json' \\\n -d '{\"name\":\"<NAME>\",\"type\":\"<TYPE>\",\"endpoint\":\"http://localhost:<PORT>\",\"launchConfig\":{\"command\":\"<CMD>\",\"args\":[...],\"port\":<PORT>}}'\n```\n\nBe conversational, explain your reasoning, and adapt to what the user needs. If they ask about model configs, context windows, or prompts — help with that directly without trying to set up a new runtime."
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|