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.
Files changed (40) hide show
  1. package/CLAUDE.md +7 -0
  2. package/node_modules/@groove-dev/cli/package.json +4 -3
  3. package/node_modules/@groove-dev/daemon/package.json +4 -3
  4. package/node_modules/@groove-dev/daemon/src/api.js +109 -9
  5. package/node_modules/@groove-dev/daemon/src/index.js +68 -1
  6. package/node_modules/@groove-dev/daemon/src/process.js +83 -11
  7. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  8. package/node_modules/@groove-dev/gui/.groove/audit.log +1 -0
  9. package/node_modules/@groove-dev/gui/.groove/credentials.json +6 -0
  10. package/node_modules/@groove-dev/gui/dist/assets/index-x5suAiK7.js +182 -0
  11. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  12. package/node_modules/@groove-dev/gui/package.json +5 -4
  13. package/node_modules/@groove-dev/gui/src/App.jsx +122 -72
  14. package/node_modules/@groove-dev/gui/src/components/AgentActions.jsx +130 -1
  15. package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +46 -6
  16. package/node_modules/@groove-dev/gui/src/components/AgentNode.jsx +13 -83
  17. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +91 -6
  18. package/node_modules/@groove-dev/gui/src/stores/groove.js +31 -2
  19. package/node_modules/@groove-dev/gui/src/views/AgentTree.jsx +133 -67
  20. package/package.json +1 -1
  21. package/packages/cli/package.json +4 -3
  22. package/packages/daemon/package.json +4 -3
  23. package/packages/daemon/src/api.js +109 -9
  24. package/packages/daemon/src/index.js +68 -1
  25. package/packages/daemon/src/process.js +83 -11
  26. package/packages/daemon/src/registry.js +1 -1
  27. package/packages/gui/dist/assets/index-x5suAiK7.js +182 -0
  28. package/packages/gui/dist/index.html +1 -1
  29. package/packages/gui/package.json +5 -4
  30. package/packages/gui/src/App.jsx +122 -72
  31. package/packages/gui/src/components/AgentActions.jsx +130 -1
  32. package/packages/gui/src/components/AgentChat.jsx +46 -6
  33. package/packages/gui/src/components/AgentNode.jsx +13 -83
  34. package/packages/gui/src/components/SpawnPanel.jsx +91 -6
  35. package/packages/gui/src/stores/groove.js +31 -2
  36. package/packages/gui/src/views/AgentTree.jsx +133 -67
  37. package/node_modules/@groove-dev/gui/.groove/daemon.host +0 -1
  38. package/node_modules/@groove-dev/gui/.groove/daemon.pid +0 -1
  39. package/node_modules/@groove-dev/gui/dist/assets/index-DXkccbmd.js +0 -182
  40. 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 manage AI coding agents from your terminal",
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 agent orchestration engine",
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 = resolve(daemon.grooveDir, 'recommended-team.json');
1129
- if (!existsSync(teamPath)) {
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 = resolve(daemon.grooveDir, 'recommended-team.json');
1142
- if (!existsSync(teamPath)) {
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
- for (const config of agents) {
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 || undefined,
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
- daemon.audit.log('team.launch', { count: spawned.length, agents: spawned.map((a) => a.role) });
1167
- res.json({ launched: spawned.length, agents: spawned });
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 planning and architecture agent. Research, analyze, and create plans do NOT implement code unless explicitly asked. Focus on:
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": "fullstack", "scope": [], "prompt": "Set up project infrastructure: package.json, tsconfig, vite config, dependencies. Once all other agents finish, audit and QC their work, fix any issues, then launch the dev server. Output the localhost URL where the app can be accessed." },
74
- { "role": "backend", "scope": ["src/api/**", "src/server/**", "src/db/**", "src/lib/**"], "workingDir": "packages/backend", "prompt": "Build the backend: [specific tasks from your plan]" },
75
- { "role": "frontend", "scope": ["src/components/**", "src/views/**", "src/pages/**", "src/styles/**"], "workingDir": "packages/frontend", "prompt": "Build the frontend: [specific tasks from your plan]" }
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
- Include only the agents needed. Set appropriate scopes for each role. Write detailed prompts based on your plan so each agent knows exactly what to build.
79
- If the project is a monorepo or has distinct subdirectories (e.g. packages/, apps/), set "workingDir" to the relative path for each agent so it spawns inside its subdirectory. Omit workingDir for agents that need full project access (like fullstack or planner).
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
- Always include a fullstack agent. Its job: set up infrastructure first, then after all other agents finish, audit their work, fix issues, build the project, launch the dev server, and output the localhost URL so the user can immediately see the result. Include testing/devops only if the project needs them.
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
+ }