groove-dev 0.18.0 → 0.18.2
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 +4 -3
- package/node_modules/@groove-dev/daemon/package.json +4 -3
- package/node_modules/@groove-dev/daemon/src/api.js +109 -9
- package/node_modules/@groove-dev/daemon/src/index.js +68 -1
- package/node_modules/@groove-dev/daemon/src/process.js +83 -11
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/gui/.groove/audit.log +1 -0
- package/node_modules/@groove-dev/gui/.groove/credentials.json +6 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-x5suAiK7.js +182 -0
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +5 -4
- package/node_modules/@groove-dev/gui/src/App.jsx +122 -72
- package/node_modules/@groove-dev/gui/src/components/AgentActions.jsx +130 -1
- package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +46 -6
- package/node_modules/@groove-dev/gui/src/components/AgentNode.jsx +13 -83
- package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +91 -6
- package/node_modules/@groove-dev/gui/src/stores/groove.js +31 -2
- package/node_modules/@groove-dev/gui/src/views/AgentTree.jsx +133 -67
- package/package.json +1 -1
- package/packages/cli/package.json +4 -3
- package/packages/daemon/package.json +4 -3
- package/packages/daemon/src/api.js +109 -9
- package/packages/daemon/src/index.js +68 -1
- package/packages/daemon/src/process.js +83 -11
- package/packages/daemon/src/registry.js +1 -1
- package/packages/gui/dist/assets/index-x5suAiK7.js +182 -0
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +5 -4
- package/packages/gui/src/App.jsx +122 -72
- package/packages/gui/src/components/AgentActions.jsx +130 -1
- package/packages/gui/src/components/AgentChat.jsx +46 -6
- package/packages/gui/src/components/AgentNode.jsx +13 -83
- package/packages/gui/src/components/SpawnPanel.jsx +91 -6
- package/packages/gui/src/stores/groove.js +31 -2
- package/packages/gui/src/views/AgentTree.jsx +133 -67
- package/node_modules/@groove-dev/gui/.groove/daemon.host +0 -1
- package/node_modules/@groove-dev/gui/.groove/daemon.pid +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DXkccbmd.js +0 -182
- package/packages/gui/dist/assets/index-DXkccbmd.js +0 -182
package/CLAUDE.md
CHANGED
|
@@ -195,3 +195,10 @@ Fully functional multi-agent orchestration system. Tested end-to-end: planner
|
|
|
195
195
|
- Remote access (--host 0.0.0.0 + token auth)
|
|
196
196
|
- Semantic degradation detection
|
|
197
197
|
- Distribution: demo video, HN launch, Twitter content
|
|
198
|
+
|
|
199
|
+
<!-- GROOVE:START -->
|
|
200
|
+
## GROOVE Orchestration (auto-injected)
|
|
201
|
+
Active agents: 0
|
|
202
|
+
See AGENTS_REGISTRY.md for full agent state.
|
|
203
|
+
**Memory policy:** Ignore auto-memory. Do not read or write MEMORY.md. GROOVE manages all context.
|
|
204
|
+
<!-- GROOVE:END -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groove-dev/cli",
|
|
3
3
|
"version": "0.11.0",
|
|
4
|
-
"description": "GROOVE CLI
|
|
4
|
+
"description": "GROOVE CLI \u2014 manage AI coding agents from your terminal",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -11,5 +11,6 @@
|
|
|
11
11
|
"@groove-dev/daemon": "*",
|
|
12
12
|
"commander": "^12.1.0",
|
|
13
13
|
"chalk": "^5.3.0"
|
|
14
|
-
}
|
|
15
|
-
|
|
14
|
+
},
|
|
15
|
+
"private": true
|
|
16
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groove-dev/daemon",
|
|
3
3
|
"version": "0.11.0",
|
|
4
|
-
"description": "GROOVE daemon
|
|
4
|
+
"description": "GROOVE daemon \u2014 agent orchestration engine",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "src/index.js",
|
|
@@ -14,5 +14,6 @@
|
|
|
14
14
|
"ws": "^8.17.0",
|
|
15
15
|
"express": "^4.21.0",
|
|
16
16
|
"minimatch": "^10.0.0"
|
|
17
|
-
}
|
|
18
|
-
|
|
17
|
+
},
|
|
18
|
+
"private": true
|
|
19
|
+
}
|
|
@@ -784,6 +784,34 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
784
784
|
}
|
|
785
785
|
});
|
|
786
786
|
|
|
787
|
+
// Browse absolute paths (for directory picker in agent config)
|
|
788
|
+
// Dirs only, localhost-only, no file content exposed
|
|
789
|
+
app.get('/api/browse-system', (req, res) => {
|
|
790
|
+
const absPath = req.query.path || process.env.HOME || '/';
|
|
791
|
+
if (absPath.includes('\0')) return res.status(400).json({ error: 'Invalid path' });
|
|
792
|
+
if (!existsSync(absPath)) return res.status(404).json({ error: 'Not found' });
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
const entries = readdirSync(absPath, { withFileTypes: true })
|
|
796
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
|
|
797
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
798
|
+
.map((e) => {
|
|
799
|
+
const full = resolve(absPath, e.name);
|
|
800
|
+
let hasChildren = false;
|
|
801
|
+
try {
|
|
802
|
+
hasChildren = readdirSync(full, { withFileTypes: true })
|
|
803
|
+
.some((c) => c.isDirectory() && !c.name.startsWith('.') && c.name !== 'node_modules');
|
|
804
|
+
} catch { /* unreadable */ }
|
|
805
|
+
return { name: e.name, path: full, hasChildren };
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const parent = absPath === '/' ? null : resolve(absPath, '..');
|
|
809
|
+
res.json({ current: absPath, parent, dirs: entries });
|
|
810
|
+
} catch (err) {
|
|
811
|
+
res.status(500).json({ error: err.message });
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
|
|
787
815
|
// --- File Editor API ---
|
|
788
816
|
|
|
789
817
|
const LANG_MAP = {
|
|
@@ -1124,9 +1152,25 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1124
1152
|
|
|
1125
1153
|
// --- Recommended Team (from planner) ---
|
|
1126
1154
|
|
|
1155
|
+
// Find recommended-team.json — check all agent working dirs, then daemon's grooveDir
|
|
1156
|
+
function findRecommendedTeam() {
|
|
1157
|
+
// Check agent working dirs first (planner may have written there)
|
|
1158
|
+
const agents = daemon.registry.getAll();
|
|
1159
|
+
for (const agent of agents) {
|
|
1160
|
+
if (agent.workingDir) {
|
|
1161
|
+
const p = resolve(agent.workingDir, '.groove', 'recommended-team.json');
|
|
1162
|
+
if (existsSync(p)) return p;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
// Fallback to daemon's .groove dir
|
|
1166
|
+
const p = resolve(daemon.grooveDir, 'recommended-team.json');
|
|
1167
|
+
if (existsSync(p)) return p;
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1127
1171
|
app.get('/api/recommended-team', (req, res) => {
|
|
1128
|
-
const teamPath =
|
|
1129
|
-
if (!
|
|
1172
|
+
const teamPath = findRecommendedTeam();
|
|
1173
|
+
if (!teamPath) {
|
|
1130
1174
|
return res.json({ exists: false, agents: [] });
|
|
1131
1175
|
}
|
|
1132
1176
|
try {
|
|
@@ -1138,8 +1182,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1138
1182
|
});
|
|
1139
1183
|
|
|
1140
1184
|
app.post('/api/recommended-team/launch', async (req, res) => {
|
|
1141
|
-
const teamPath =
|
|
1142
|
-
if (!
|
|
1185
|
+
const teamPath = findRecommendedTeam();
|
|
1186
|
+
if (!teamPath) {
|
|
1143
1187
|
return res.status(404).json({ error: 'No recommended team found. Run a planner first.' });
|
|
1144
1188
|
}
|
|
1145
1189
|
try {
|
|
@@ -1148,8 +1192,24 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1148
1192
|
return res.status(400).json({ error: 'Recommended team is empty' });
|
|
1149
1193
|
}
|
|
1150
1194
|
|
|
1195
|
+
const defaultDir = daemon.config?.defaultWorkingDir || undefined;
|
|
1196
|
+
|
|
1197
|
+
// Separate phase 1 (builders) and phase 2 (QC/finisher)
|
|
1198
|
+
const phase1 = agents.filter((a) => !a.phase || a.phase === 1);
|
|
1199
|
+
let phase2 = agents.filter((a) => a.phase === 2);
|
|
1200
|
+
|
|
1201
|
+
// Safety net: if planner forgot the QC agent, auto-add one
|
|
1202
|
+
if (phase2.length === 0 && phase1.length >= 2) {
|
|
1203
|
+
phase2 = [{
|
|
1204
|
+
role: 'fullstack', phase: 2, scope: [],
|
|
1205
|
+
prompt: 'QC Senior Dev: All builder agents have completed. Audit their changes for correctness, fix any issues, run tests, build the project, commit all changes, and launch. Output the localhost URL where the app can be accessed.',
|
|
1206
|
+
}];
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Spawn phase 1 agents immediately
|
|
1151
1210
|
const spawned = [];
|
|
1152
|
-
|
|
1211
|
+
const phase1Ids = [];
|
|
1212
|
+
for (const config of phase1) {
|
|
1153
1213
|
const validated = validateAgentConfig({
|
|
1154
1214
|
role: config.role,
|
|
1155
1215
|
scope: config.scope || [],
|
|
@@ -1157,19 +1217,59 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1157
1217
|
provider: config.provider || 'claude-code',
|
|
1158
1218
|
model: config.model || 'auto',
|
|
1159
1219
|
permission: config.permission || 'auto',
|
|
1160
|
-
workingDir: config.workingDir ||
|
|
1220
|
+
workingDir: config.workingDir || defaultDir,
|
|
1221
|
+
name: config.name || undefined,
|
|
1161
1222
|
});
|
|
1162
1223
|
const agent = await daemon.processes.spawn(validated);
|
|
1163
1224
|
spawned.push({ id: agent.id, name: agent.name, role: agent.role });
|
|
1225
|
+
phase1Ids.push(agent.id);
|
|
1164
1226
|
}
|
|
1165
1227
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1228
|
+
// If there are phase 2 agents, register them for auto-spawn on phase 1 completion
|
|
1229
|
+
if (phase2.length > 0 && phase1Ids.length > 0) {
|
|
1230
|
+
daemon._pendingPhase2 = daemon._pendingPhase2 || [];
|
|
1231
|
+
daemon._pendingPhase2.push({
|
|
1232
|
+
waitFor: phase1Ids,
|
|
1233
|
+
agents: phase2.map((c) => ({
|
|
1234
|
+
role: c.role, scope: c.scope || [], prompt: c.prompt || '',
|
|
1235
|
+
provider: c.provider || 'claude-code', model: c.model || 'auto',
|
|
1236
|
+
permission: c.permission || 'auto',
|
|
1237
|
+
workingDir: c.workingDir || defaultDir,
|
|
1238
|
+
name: c.name || undefined,
|
|
1239
|
+
})),
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
daemon.audit.log('team.launch', {
|
|
1244
|
+
phase1: spawned.length, phase2Pending: phase2.length,
|
|
1245
|
+
agents: spawned.map((a) => a.role),
|
|
1246
|
+
});
|
|
1247
|
+
res.json({ launched: spawned.length, phase2Pending: phase2.length, agents: spawned });
|
|
1168
1248
|
} catch (err) {
|
|
1169
1249
|
res.status(500).json({ error: err.message });
|
|
1170
1250
|
}
|
|
1171
1251
|
});
|
|
1172
1252
|
|
|
1253
|
+
// Clean up stale artifacts (old plans, recommended teams, etc.)
|
|
1254
|
+
app.post('/api/cleanup', (req, res) => {
|
|
1255
|
+
let cleaned = 0;
|
|
1256
|
+
// Clean recommended-team.json from all known locations
|
|
1257
|
+
const locations = [resolve(daemon.grooveDir, 'recommended-team.json')];
|
|
1258
|
+
for (const agent of daemon.registry.getAll()) {
|
|
1259
|
+
if (agent.workingDir) {
|
|
1260
|
+
locations.push(resolve(agent.workingDir, '.groove', 'recommended-team.json'));
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
const defaultDir = daemon.config?.defaultWorkingDir;
|
|
1264
|
+
if (defaultDir) locations.push(resolve(defaultDir, '.groove', 'recommended-team.json'));
|
|
1265
|
+
|
|
1266
|
+
for (const p of locations) {
|
|
1267
|
+
if (existsSync(p)) { try { unlinkSync(p); cleaned++; } catch { /* */ } }
|
|
1268
|
+
}
|
|
1269
|
+
daemon.audit.log('cleanup', { cleaned });
|
|
1270
|
+
res.json({ ok: true, cleaned });
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1173
1273
|
// --- Command Center Dashboard ---
|
|
1174
1274
|
|
|
1175
1275
|
app.get('/api/dashboard', (req, res) => {
|
|
@@ -1350,7 +1450,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1350
1450
|
app.patch('/api/config', async (req, res) => {
|
|
1351
1451
|
const ALLOWED_KEYS = [
|
|
1352
1452
|
'port', 'journalistInterval', 'rotationThreshold', 'autoRotation',
|
|
1353
|
-
'qcThreshold', 'maxAgents', 'defaultProvider',
|
|
1453
|
+
'qcThreshold', 'maxAgents', 'defaultProvider', 'defaultWorkingDir',
|
|
1354
1454
|
];
|
|
1355
1455
|
for (const key of Object.keys(req.body)) {
|
|
1356
1456
|
if (!ALLOWED_KEYS.includes(key)) {
|
|
@@ -5,7 +5,7 @@ import { createServer as createHttpServer } from 'http';
|
|
|
5
5
|
import { createServer as createNetServer } from 'net';
|
|
6
6
|
import { execFileSync } from 'child_process';
|
|
7
7
|
import { resolve } from 'path';
|
|
8
|
-
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
|
8
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
9
9
|
import express from 'express';
|
|
10
10
|
import { WebSocketServer } from 'ws';
|
|
11
11
|
import { Registry } from './registry.js';
|
|
@@ -247,6 +247,15 @@ export class Daemon {
|
|
|
247
247
|
tester.listen(port, bindHost);
|
|
248
248
|
}).catch(() => false);
|
|
249
249
|
|
|
250
|
+
if (!(await checkPort(this.port))) {
|
|
251
|
+
// Wait for port release (e.g., after groove stop)
|
|
252
|
+
let retries = 5;
|
|
253
|
+
while (retries > 0 && !(await checkPort(this.port))) {
|
|
254
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
255
|
+
retries--;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
250
259
|
if (!(await checkPort(this.port))) {
|
|
251
260
|
const originalPort = this.port;
|
|
252
261
|
// Try next 10 ports
|
|
@@ -281,6 +290,7 @@ export class Daemon {
|
|
|
281
290
|
this.journalist.start();
|
|
282
291
|
this.rotator.start();
|
|
283
292
|
this.scheduler.start();
|
|
293
|
+
this._startGarbageCollector();
|
|
284
294
|
|
|
285
295
|
// Scan codebase for workspace/structure awareness
|
|
286
296
|
this.indexer.scan();
|
|
@@ -290,6 +300,62 @@ export class Daemon {
|
|
|
290
300
|
});
|
|
291
301
|
}
|
|
292
302
|
|
|
303
|
+
_startGarbageCollector() {
|
|
304
|
+
// Run once on startup, then every 24 hours
|
|
305
|
+
this._gc();
|
|
306
|
+
this._gcInterval = setInterval(() => this._gc(), 24 * 60 * 60 * 1000);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_gc() {
|
|
310
|
+
const { grooveDir } = this;
|
|
311
|
+
let cleaned = 0;
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
// 1. Clean old log files (>7 days, agent no longer exists)
|
|
315
|
+
const logsDir = resolve(grooveDir, 'logs');
|
|
316
|
+
if (existsSync(logsDir)) {
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
const sevenDays = 7 * 24 * 60 * 60 * 1000;
|
|
319
|
+
for (const file of readdirSync(logsDir)) {
|
|
320
|
+
const p = resolve(logsDir, file);
|
|
321
|
+
try {
|
|
322
|
+
const age = now - statSync(p).mtimeMs;
|
|
323
|
+
if (age > sevenDays) { unlinkSync(p); cleaned++; }
|
|
324
|
+
} catch { /* skip */ }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 2. Clean stale recommended-team.json from daemon dir (not working dirs — those are user-managed)
|
|
329
|
+
// Only clean if no planner agent is currently running
|
|
330
|
+
const hasPlanner = this.registry.getAll().some((a) => a.role === 'planner' && (a.status === 'running' || a.status === 'starting'));
|
|
331
|
+
if (!hasPlanner) {
|
|
332
|
+
const teamFile = resolve(grooveDir, 'recommended-team.json');
|
|
333
|
+
if (existsSync(teamFile)) {
|
|
334
|
+
try {
|
|
335
|
+
const age = Date.now() - statSync(teamFile).mtimeMs;
|
|
336
|
+
if (age > 24 * 60 * 60 * 1000) { unlinkSync(teamFile); cleaned++; } // >24h old
|
|
337
|
+
} catch { /* skip */ }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 3. Prune audit log (keep last 1000 lines)
|
|
342
|
+
const auditFile = resolve(grooveDir, 'audit.log');
|
|
343
|
+
if (existsSync(auditFile)) {
|
|
344
|
+
try {
|
|
345
|
+
const lines = readFileSync(auditFile, 'utf8').split('\n');
|
|
346
|
+
if (lines.length > 1000) {
|
|
347
|
+
writeFileSync(auditFile, lines.slice(-1000).join('\n'));
|
|
348
|
+
cleaned++;
|
|
349
|
+
}
|
|
350
|
+
} catch { /* skip */ }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (cleaned > 0) {
|
|
354
|
+
this.audit.log('gc.run', { cleaned });
|
|
355
|
+
}
|
|
356
|
+
} catch { /* gc should never crash the daemon */ }
|
|
357
|
+
}
|
|
358
|
+
|
|
293
359
|
async stop() {
|
|
294
360
|
// Persist state before shutdown
|
|
295
361
|
this.state.set('agents', this.registry.getAll());
|
|
@@ -299,6 +365,7 @@ export class Daemon {
|
|
|
299
365
|
this.journalist.stop();
|
|
300
366
|
this.rotator.stop();
|
|
301
367
|
this.scheduler.stop();
|
|
368
|
+
if (this._gcInterval) clearInterval(this._gcInterval);
|
|
302
369
|
|
|
303
370
|
// Clean up file watchers and terminal sessions
|
|
304
371
|
this.fileWatcher.unwatchAll();
|
|
@@ -2,9 +2,10 @@
|
|
|
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 } from 'fs';
|
|
5
|
+
import { createWriteStream, mkdirSync, chmodSync, existsSync, readFileSync, unlinkSync } from 'fs';
|
|
6
6
|
import { resolve } from 'path';
|
|
7
7
|
import { getProvider } from './providers/index.js';
|
|
8
|
+
import { validateAgentConfig } from './validate.js';
|
|
8
9
|
|
|
9
10
|
// Role-specific prompt prefixes — applied during spawn regardless of entry point
|
|
10
11
|
// (SpawnPanel, chat continue, CLI, API) for consistency
|
|
@@ -58,11 +59,15 @@ Do NOT write code unless explicitly asked. Use your MCP tools (database queries,
|
|
|
58
59
|
Do NOT write code unless explicitly asked. Use your MCP tools to interact with Home Assistant.
|
|
59
60
|
|
|
60
61
|
`,
|
|
61
|
-
planner: `You are a
|
|
62
|
+
planner: `You are a PLANNING ONLY agent. You create plans. You do NOT write code, edit files, or run commands.
|
|
63
|
+
|
|
64
|
+
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.
|
|
65
|
+
|
|
66
|
+
Focus on:
|
|
62
67
|
- Understanding requirements
|
|
63
|
-
- Exploring the codebase
|
|
68
|
+
- Exploring the codebase to understand current architecture
|
|
64
69
|
- Identifying approaches and trade-offs
|
|
65
|
-
- Writing structured plans
|
|
70
|
+
- Writing structured plans with agent assignments
|
|
66
71
|
|
|
67
72
|
After completing your plan, you MUST do two things:
|
|
68
73
|
|
|
@@ -70,15 +75,25 @@ After completing your plan, you MUST do two things:
|
|
|
70
75
|
|
|
71
76
|
2. Save a machine-readable team config to .groove/recommended-team.json using this EXACT format:
|
|
72
77
|
[
|
|
73
|
-
{ "role": "
|
|
74
|
-
{ "role": "backend", "
|
|
75
|
-
{ "role": "
|
|
78
|
+
{ "role": "frontend", "phase": 1, "scope": ["src/components/**", "src/views/**"], "prompt": "Build the frontend: [specific tasks]" },
|
|
79
|
+
{ "role": "backend", "phase": 1, "scope": ["src/api/**", "src/server/**"], "prompt": "Build the backend: [specific tasks]" },
|
|
80
|
+
{ "role": "fullstack", "phase": 2, "scope": [], "prompt": "QC Senior Dev: Audit all changes from phase 1 agents. Verify correctness, fix issues, run tests, build the project, commit, and launch. Output the localhost URL." }
|
|
76
81
|
]
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
MANDATORY RULES — NEVER SKIP THESE:
|
|
84
|
+
|
|
85
|
+
1. The LAST entry in the array MUST be: { "role": "fullstack", "phase": 2, ... }
|
|
86
|
+
This is the QC Senior Dev. It auto-spawns after all other agents finish.
|
|
87
|
+
Its prompt: audit changes, fix issues, run tests, build, commit, launch.
|
|
88
|
+
NEVER omit this agent. Every team needs a QC.
|
|
89
|
+
|
|
90
|
+
2. ALL other agents are phase: 1 — they run in parallel.
|
|
91
|
+
|
|
92
|
+
3. Do NOT tell any agent to "wait for" another agent. Phase 2 handles sequencing automatically.
|
|
93
|
+
|
|
94
|
+
4. Set appropriate scopes. Write detailed prompts so each agent knows exactly what to build.
|
|
80
95
|
|
|
81
|
-
|
|
96
|
+
5. If the project is a monorepo, set "workingDir" for agents that need specific subdirectories.
|
|
82
97
|
|
|
83
98
|
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.
|
|
84
99
|
|
|
@@ -105,6 +120,17 @@ export class ProcessManager {
|
|
|
105
120
|
async spawn(config) {
|
|
106
121
|
const { registry, locks, introducer } = this.daemon;
|
|
107
122
|
|
|
123
|
+
// Clean stale recommended-team.json when spawning a new planner
|
|
124
|
+
if (config.role === 'planner') {
|
|
125
|
+
const dirs = [this.daemon.grooveDir];
|
|
126
|
+
if (config.workingDir) dirs.push(resolve(config.workingDir, '.groove'));
|
|
127
|
+
if (this.daemon.config?.defaultWorkingDir) dirs.push(resolve(this.daemon.config.defaultWorkingDir, '.groove'));
|
|
128
|
+
for (const dir of dirs) {
|
|
129
|
+
const p = resolve(dir, 'recommended-team.json');
|
|
130
|
+
if (existsSync(p)) try { unlinkSync(p); } catch { /* */ }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
108
134
|
// Validate provider exists and is installed
|
|
109
135
|
const provider = getProvider(config.provider || 'claude-code');
|
|
110
136
|
if (!provider) {
|
|
@@ -325,6 +351,9 @@ For normal file edits within your scope, proceed without review.
|
|
|
325
351
|
if (finalStatus === 'completed' && this.daemon.journalist) {
|
|
326
352
|
this.daemon.journalist.cycle().catch(() => {});
|
|
327
353
|
}
|
|
354
|
+
|
|
355
|
+
// Phase 2 auto-spawn: check if all phase 1 agents for a team are done
|
|
356
|
+
this._checkPhase2(agent.id);
|
|
328
357
|
});
|
|
329
358
|
|
|
330
359
|
proc.on('error', (err) => {
|
|
@@ -338,6 +367,49 @@ For normal file edits within your scope, proceed without review.
|
|
|
338
367
|
return agent;
|
|
339
368
|
}
|
|
340
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Check if a completed/crashed agent was the last phase 1 agent in a team.
|
|
372
|
+
* If so, auto-spawn the phase 2 (QC/finisher) agents.
|
|
373
|
+
*/
|
|
374
|
+
_checkPhase2(completedAgentId) {
|
|
375
|
+
const pending = this.daemon._pendingPhase2;
|
|
376
|
+
if (!pending || pending.length === 0) return;
|
|
377
|
+
|
|
378
|
+
const registry = this.daemon.registry;
|
|
379
|
+
|
|
380
|
+
for (let i = pending.length - 1; i >= 0; i--) {
|
|
381
|
+
const group = pending[i];
|
|
382
|
+
if (!group.waitFor.includes(completedAgentId)) continue;
|
|
383
|
+
|
|
384
|
+
// Check if ALL phase 1 agents in this group are done
|
|
385
|
+
const allDone = group.waitFor.every((id) => {
|
|
386
|
+
const a = registry.get(id);
|
|
387
|
+
return !a || a.status === 'completed' || a.status === 'crashed' || a.status === 'stopped' || a.status === 'killed';
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
if (allDone) {
|
|
391
|
+
// Remove from pending
|
|
392
|
+
pending.splice(i, 1);
|
|
393
|
+
|
|
394
|
+
// Auto-spawn phase 2 agents
|
|
395
|
+
for (const config of group.agents) {
|
|
396
|
+
try {
|
|
397
|
+
const validated = validateAgentConfig(config);
|
|
398
|
+
this.spawn(validated).then((agent) => {
|
|
399
|
+
this.daemon.broadcast({
|
|
400
|
+
type: 'phase2:spawned',
|
|
401
|
+
agentId: agent.id,
|
|
402
|
+
name: agent.name,
|
|
403
|
+
role: agent.role,
|
|
404
|
+
});
|
|
405
|
+
this.daemon.audit.log('phase2.autoSpawn', { id: agent.id, name: agent.name, role: agent.role });
|
|
406
|
+
}).catch(() => {});
|
|
407
|
+
} catch { /* skip invalid configs */ }
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
341
413
|
/**
|
|
342
414
|
* Resume a completed agent's session with a new message.
|
|
343
415
|
* Uses --resume SESSION_ID for zero cold-start continuation.
|
|
@@ -390,7 +462,7 @@ For normal file edits within your scope, proceed without review.
|
|
|
390
462
|
model: config.model,
|
|
391
463
|
prompt: config.prompt,
|
|
392
464
|
permission: config.permission,
|
|
393
|
-
workingDir: config.workingDir,
|
|
465
|
+
workingDir: config.workingDir || this.daemon.config?.defaultWorkingDir || undefined,
|
|
394
466
|
name: config.name,
|
|
395
467
|
});
|
|
396
468
|
|
|
@@ -50,7 +50,7 @@ export class Registry extends EventEmitter {
|
|
|
50
50
|
if (!agent) return null;
|
|
51
51
|
|
|
52
52
|
// Only allow known fields to prevent prototype pollution
|
|
53
|
-
const SAFE_FIELDS = ['status', 'pid', 'tokensUsed', 'contextUsage', 'lastActivity', 'model', 'name', 'routingMode', 'routingReason', 'sessionId', 'skills', 'integrations'];
|
|
53
|
+
const SAFE_FIELDS = ['status', 'pid', 'tokensUsed', 'contextUsage', 'lastActivity', 'model', 'name', 'routingMode', 'routingReason', 'sessionId', 'skills', 'integrations', 'workingDir', 'effort'];
|
|
54
54
|
for (const key of Object.keys(updates)) {
|
|
55
55
|
if (SAFE_FIELDS.includes(key)) {
|
|
56
56
|
agent[key] = updates[key];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"t":"2026-04-08T02:20:01.108Z","action":"credential.set","provider":"anthropic-api"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"anthropic-api": {
|
|
3
|
+
"key": "bee44e798e85b64b04e5198bd44074f0:a0de1b79cca1a00842ca83193e383128:fb7b942b82313c940f802fd2c54f5945756ce6b65dae652b1534acd38d9cca19cdfaad8eee267f56e11f2c2ec6cd8db16c4a4139ddfe0777517437a3a4b5beb53e89210e31c1cfc1f964bba8b26ca75b93f62d36ea1dbc19d3910a91c8a976a2c4369b233a5d00f799e33ebd",
|
|
4
|
+
"setAt": "2026-04-08T02:20:01.107Z"
|
|
5
|
+
}
|
|
6
|
+
}
|