groove-dev 0.17.8 → 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 (131) hide show
  1. package/node_modules/@groove-dev/cli/package.json +4 -3
  2. package/node_modules/@groove-dev/daemon/google-oauth.json +5 -0
  3. package/node_modules/@groove-dev/daemon/integrations-registry.json +0 -40
  4. package/node_modules/@groove-dev/daemon/package.json +4 -3
  5. package/node_modules/@groove-dev/daemon/src/api.js +212 -21
  6. package/node_modules/@groove-dev/daemon/src/index.js +68 -1
  7. package/node_modules/@groove-dev/daemon/src/integrations.js +59 -20
  8. package/node_modules/@groove-dev/daemon/src/process.js +83 -11
  9. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +4 -0
  10. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  11. package/node_modules/@groove-dev/gui/.groove/audit.log +1 -0
  12. package/node_modules/@groove-dev/gui/.groove/codebase-index.json +64 -0
  13. package/node_modules/@groove-dev/gui/.groove/config.json +10 -0
  14. package/node_modules/@groove-dev/gui/.groove/coordination.md +5 -0
  15. package/node_modules/@groove-dev/gui/.groove/credentials.json +6 -0
  16. package/node_modules/@groove-dev/gui/.groove/daemon.port +1 -0
  17. package/node_modules/@groove-dev/gui/.groove/federation/identity.key +3 -0
  18. package/node_modules/@groove-dev/gui/.groove/federation/identity.pub +3 -0
  19. package/node_modules/@groove-dev/gui/.groove/integrations/package.json +6 -0
  20. package/node_modules/@groove-dev/gui/.groove/state.json +3 -0
  21. package/node_modules/@groove-dev/gui/dist/assets/index-x5suAiK7.js +182 -0
  22. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  23. package/node_modules/@groove-dev/gui/package.json +5 -4
  24. package/node_modules/@groove-dev/gui/src/App.jsx +149 -76
  25. package/node_modules/@groove-dev/gui/src/components/AgentActions.jsx +130 -1
  26. package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +47 -7
  27. package/node_modules/@groove-dev/gui/src/components/AgentNode.jsx +13 -83
  28. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +918 -580
  29. package/node_modules/@groove-dev/gui/src/stores/groove.js +31 -2
  30. package/node_modules/@groove-dev/gui/src/views/AgentTree.jsx +133 -67
  31. package/node_modules/@groove-dev/gui/src/views/FileEditor.jsx +85 -1
  32. package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +121 -44
  33. package/package.json +1 -2
  34. package/packages/cli/package.json +4 -3
  35. package/packages/daemon/integrations-registry.json +0 -40
  36. package/packages/daemon/package.json +4 -3
  37. package/packages/daemon/src/api.js +212 -21
  38. package/packages/daemon/src/index.js +68 -1
  39. package/packages/daemon/src/integrations.js +59 -20
  40. package/packages/daemon/src/process.js +83 -11
  41. package/packages/daemon/src/providers/claude-code.js +4 -0
  42. package/packages/daemon/src/registry.js +1 -1
  43. package/packages/gui/dist/assets/index-x5suAiK7.js +182 -0
  44. package/packages/gui/dist/index.html +1 -1
  45. package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js +68 -0
  46. package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js.map +7 -0
  47. package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js +1420 -0
  48. package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js.map +7 -0
  49. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js +17 -0
  50. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js.map +7 -0
  51. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +22 -0
  52. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js.map +7 -0
  53. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +34 -0
  54. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js.map +7 -0
  55. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js +101 -0
  56. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js.map +7 -0
  57. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +2534 -0
  58. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js.map +7 -0
  59. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +789 -0
  60. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js.map +7 -0
  61. package/packages/gui/node_modules/.vite/deps/@codemirror_language.js +115 -0
  62. package/packages/gui/node_modules/.vite/deps/@codemirror_language.js.map +7 -0
  63. package/packages/gui/node_modules/.vite/deps/@codemirror_search.js +1136 -0
  64. package/packages/gui/node_modules/.vite/deps/@codemirror_search.js.map +7 -0
  65. package/packages/gui/node_modules/.vite/deps/@codemirror_state.js +63 -0
  66. package/packages/gui/node_modules/.vite/deps/@codemirror_state.js.map +7 -0
  67. package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js +179 -0
  68. package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js.map +7 -0
  69. package/packages/gui/node_modules/.vite/deps/@codemirror_view.js +104 -0
  70. package/packages/gui/node_modules/.vite/deps/@codemirror_view.js.map +7 -0
  71. package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js +46 -0
  72. package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js.map +7 -0
  73. package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js +121 -0
  74. package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js.map +7 -0
  75. package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js +9237 -0
  76. package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js.map +7 -0
  77. package/packages/gui/node_modules/.vite/deps/@xyflow_react.js +9934 -0
  78. package/packages/gui/node_modules/.vite/deps/@xyflow_react.js.map +7 -0
  79. package/packages/gui/node_modules/.vite/deps/_metadata.json +184 -0
  80. package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js +5169 -0
  81. package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js.map +7 -0
  82. package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js +2000 -0
  83. package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js.map +7 -0
  84. package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js +1115 -0
  85. package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js.map +7 -0
  86. package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js +701 -0
  87. package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js.map +7 -0
  88. package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js +1776 -0
  89. package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js.map +7 -0
  90. package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js +280 -0
  91. package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js.map +7 -0
  92. package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
  93. package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
  94. package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js +1004 -0
  95. package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js.map +7 -0
  96. package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js +292 -0
  97. package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js.map +7 -0
  98. package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js +1062 -0
  99. package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js.map +7 -0
  100. package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js +10985 -0
  101. package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js.map +7 -0
  102. package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js +3459 -0
  103. package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js.map +7 -0
  104. package/packages/gui/node_modules/.vite/deps/package.json +3 -0
  105. package/packages/gui/node_modules/.vite/deps/react-dom.js +6 -0
  106. package/packages/gui/node_modules/.vite/deps/react-dom.js.map +7 -0
  107. package/packages/gui/node_modules/.vite/deps/react-dom_client.js +20217 -0
  108. package/packages/gui/node_modules/.vite/deps/react-dom_client.js.map +7 -0
  109. package/packages/gui/node_modules/.vite/deps/react.js +5 -0
  110. package/packages/gui/node_modules/.vite/deps/react.js.map +7 -0
  111. package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js +278 -0
  112. package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  113. package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js +6 -0
  114. package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
  115. package/packages/gui/node_modules/.vite/deps/zustand.js +56 -0
  116. package/packages/gui/node_modules/.vite/deps/zustand.js.map +7 -0
  117. package/packages/gui/package.json +5 -4
  118. package/packages/gui/src/App.jsx +149 -76
  119. package/packages/gui/src/components/AgentActions.jsx +130 -1
  120. package/packages/gui/src/components/AgentChat.jsx +47 -7
  121. package/packages/gui/src/components/AgentNode.jsx +13 -83
  122. package/packages/gui/src/components/SpawnPanel.jsx +918 -580
  123. package/packages/gui/src/stores/groove.js +31 -2
  124. package/packages/gui/src/views/AgentTree.jsx +133 -67
  125. package/packages/gui/src/views/FileEditor.jsx +85 -1
  126. package/packages/gui/src/views/IntegrationsStore.jsx +121 -44
  127. package/docs/FILE-EDITOR-PLAN.md +0 -253
  128. package/docs/GUI_DESIGN_SPEC.md +0 -402
  129. package/docs/SKILLS-API-SPEC.md +0 -277
  130. package/node_modules/@groove-dev/gui/dist/assets/index-D5dtDQf0.js +0 -156
  131. package/packages/gui/dist/assets/index-D5dtDQf0.js +0 -156
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "client_id": "",
3
+ "client_secret": "",
4
+ "_comment": "Fill in with your Google Cloud OAuth Desktop App credentials. One-time setup — all Google integrations (Gmail, Calendar, Drive) use these."
5
+ }
@@ -338,26 +338,6 @@
338
338
  "ratingCount": 0,
339
339
  "verified": "community"
340
340
  },
341
- {
342
- "id": "filesystem",
343
- "name": "Filesystem",
344
- "description": "Read, write, search, and manage files on the local filesystem",
345
- "category": "developer",
346
- "icon": "folder",
347
- "tags": ["files", "filesystem", "local", "storage"],
348
- "roles": ["backend", "fullstack", "devops"],
349
- "npmPackage": "@modelcontextprotocol/server-filesystem",
350
- "transport": "stdio",
351
- "command": "npx",
352
- "args": ["-y", "@modelcontextprotocol/server-filesystem"],
353
- "authType": "none",
354
- "envKeys": [],
355
- "featured": false,
356
- "downloads": 0,
357
- "rating": 0,
358
- "ratingCount": 0,
359
- "verified": "mcp-official"
360
- },
361
341
  {
362
342
  "id": "google-maps",
363
343
  "name": "Google Maps",
@@ -385,25 +365,5 @@
385
365
  "rating": 0,
386
366
  "ratingCount": 0,
387
367
  "verified": "mcp-official"
388
- },
389
- {
390
- "id": "sqlite",
391
- "name": "SQLite",
392
- "description": "Query and manage SQLite databases, inspect schemas, run SQL",
393
- "category": "database",
394
- "icon": "database",
395
- "tags": ["sql", "database", "local", "lightweight"],
396
- "roles": ["analyst", "backend"],
397
- "npmPackage": "mcp-sqlite",
398
- "transport": "stdio",
399
- "command": "npx",
400
- "args": ["-y", "mcp-sqlite"],
401
- "authType": "none",
402
- "envKeys": [],
403
- "featured": false,
404
- "downloads": 0,
405
- "rating": 0,
406
- "ratingCount": 0,
407
- "verified": "mcp-official"
408
368
  }
409
369
  ]
@@ -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
+ }
@@ -5,8 +5,9 @@ import express from 'express';
5
5
  import { resolve, dirname } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream } from 'fs';
8
+ import { spawn as cpSpawn } from 'child_process';
8
9
  import { lookup as mimeLookup } from './mimetypes.js';
9
- import { listProviders } from './providers/index.js';
10
+ import { listProviders, getProvider } from './providers/index.js';
10
11
  import { validateAgentConfig } from './validate.js';
11
12
 
12
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -351,6 +352,67 @@ export function createApi(app, daemon) {
351
352
  });
352
353
  });
353
354
 
355
+ // Plan chat — direct API (fast, sub-second) when API key available, CLI fallback otherwise
356
+ const PLAN_SYSTEM = `You are the planning assistant built into Groove's spawn panel. The user is configuring an AI agent right now — they're looking at a form with role selection, file scope, skills, integrations, effort level, and a task prompt. Your conversation will be synthesized into the agent's task prompt when they click "Generate Prompt."
357
+
358
+ Your job: help them think through what the agent should do, then craft a clear plan. Be direct and practical. Don't ask how they'll feed input to agents or what tools to use — they're already inside Groove doing it. Focus on the TASK itself.
359
+
360
+ What you know about the system:
361
+ - The user is in the spawn panel, configuring an agent before launching it
362
+ - The left panel has: role picker, directory, permissions, effort, integrations, skills, schedule
363
+ - When done planning, "Generate Prompt" synthesizes this chat into the agent's task prompt
364
+ - Agents are Claude Code instances with full terminal/file access in the specified directory
365
+ - Agents can read/write files, run commands, use MCP integrations (Slack, GitHub, etc.)
366
+ - The journalist system prevents cold starts during context rotation — agents don't lose context
367
+
368
+ Keep responses concise. Help them think, don't lecture them about the system they built.`;
369
+
370
+ app.post('/api/journalist/query', async (req, res) => {
371
+ try {
372
+ const { prompt } = req.body || {};
373
+ if (!prompt) return res.status(400).json({ error: 'prompt is required' });
374
+
375
+ // Fast path: direct Anthropic API call (sub-second)
376
+ const apiKey = daemon.credentials.getKey('anthropic-api');
377
+ if (apiKey) {
378
+ const apiRes = await fetch('https://api.anthropic.com/v1/messages', {
379
+ method: 'POST',
380
+ headers: {
381
+ 'Content-Type': 'application/json',
382
+ 'x-api-key': apiKey,
383
+ 'anthropic-version': '2023-06-01',
384
+ },
385
+ body: JSON.stringify({
386
+ model: 'claude-haiku-4-5-20251001',
387
+ max_tokens: 1024,
388
+ system: PLAN_SYSTEM,
389
+ messages: [{ role: 'user', content: prompt }],
390
+ }),
391
+ });
392
+ const data = await apiRes.json();
393
+ if (data.content?.[0]?.text) {
394
+ return res.json({ response: data.content[0].text, mode: 'fast' });
395
+ }
396
+ if (data.error) {
397
+ return res.status(400).json({ error: data.error.message || 'API error' });
398
+ }
399
+ }
400
+
401
+ // Slow path: CLI fallback for subscription auth (~10s)
402
+ const fullPrompt = `${PLAN_SYSTEM}\n\n${prompt}`;
403
+ const response = await daemon.journalist.callHeadless(fullPrompt);
404
+ res.json({ response, mode: 'cli' });
405
+ } catch (err) {
406
+ res.status(500).json({ error: err.message });
407
+ }
408
+ });
409
+
410
+ // Check if Anthropic API key is configured
411
+ app.get('/api/anthropic-key/status', (req, res) => {
412
+ const hasKey = !!daemon.credentials.getKey('anthropic-api');
413
+ res.json({ configured: hasKey });
414
+ });
415
+
354
416
  // Trigger journalist cycle manually
355
417
  app.post('/api/journalist/cycle', async (req, res) => {
356
418
  try {
@@ -507,11 +569,13 @@ export function createApi(app, daemon) {
507
569
  // Parameterized :id routes (after specific routes above)
508
570
 
509
571
  app.post('/api/integrations/:id/authenticate', (req, res) => {
572
+ console.log(`[Groove:API] POST /api/integrations/${req.params.id}/authenticate`);
510
573
  try {
511
574
  const handle = daemon.integrations.authenticate(req.params.id);
575
+ console.log(`[Groove:API] Authenticate started, PID: ${handle.pid}`);
512
576
  res.json({ ok: true, pid: handle.pid });
513
- // Auto-cleanup tracked by the handle timeout
514
577
  } catch (err) {
578
+ console.log(`[Groove:API] Authenticate error: ${err.message}`);
515
579
  res.status(400).json({ error: err.message });
516
580
  }
517
581
  });
@@ -720,6 +784,34 @@ export function createApi(app, daemon) {
720
784
  }
721
785
  });
722
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
+
723
815
  // --- File Editor API ---
724
816
 
725
817
  const LANG_MAP = {
@@ -740,6 +832,11 @@ export function createApi(app, daemon) {
740
832
 
741
833
  const IGNORED_NAMES = new Set(['.git', 'node_modules', '.DS_Store', '.groove', '__pycache__', '.next', '.cache', 'dist', 'coverage']);
742
834
 
835
+ // Editor root directory — defaults to projectDir but can be changed at runtime
836
+ let editorRootDir = daemon.projectDir;
837
+
838
+ function getEditorRoot() { return editorRootDir; }
839
+
743
840
  function validateFilePath(relPath, projectDir) {
744
841
  if (!relPath || typeof relPath !== 'string') return { error: 'path is required' };
745
842
  if (relPath.startsWith('/') || relPath.includes('..') || relPath.includes('\0')) {
@@ -750,6 +847,27 @@ export function createApi(app, daemon) {
750
847
  return { fullPath };
751
848
  }
752
849
 
850
+ // Get/set the editor working directory
851
+ app.get('/api/files/root', (req, res) => {
852
+ res.json({ root: editorRootDir });
853
+ });
854
+
855
+ app.post('/api/files/root', (req, res) => {
856
+ const { root } = req.body || {};
857
+ if (!root || typeof root !== 'string') return res.status(400).json({ error: 'root path is required' });
858
+ // Must be absolute and exist
859
+ if (!root.startsWith('/')) return res.status(400).json({ error: 'root must be an absolute path' });
860
+ if (root.includes('\0') || root.includes('..')) return res.status(400).json({ error: 'Invalid path' });
861
+ if (!existsSync(root)) return res.status(404).json({ error: 'Directory not found' });
862
+ try {
863
+ const stat = statSync(root);
864
+ if (!stat.isDirectory()) return res.status(400).json({ error: 'Path is not a directory' });
865
+ } catch { return res.status(400).json({ error: 'Cannot access directory' }); }
866
+ editorRootDir = root;
867
+ daemon.audit.log('editor.root.set', { root });
868
+ res.json({ ok: true, root: editorRootDir });
869
+ });
870
+
753
871
  // File tree — returns dirs + files for a given path
754
872
  app.get('/api/files/tree', (req, res) => {
755
873
  const relPath = req.query.path || '';
@@ -759,8 +877,9 @@ export function createApi(app, daemon) {
759
877
  return res.status(400).json({ error: 'Invalid path' });
760
878
  }
761
879
 
762
- const fullPath = relPath ? resolve(daemon.projectDir, relPath) : daemon.projectDir;
763
- if (!fullPath.startsWith(daemon.projectDir)) {
880
+ const rootDir = getEditorRoot();
881
+ const fullPath = relPath ? resolve(rootDir, relPath) : rootDir;
882
+ if (!fullPath.startsWith(rootDir)) {
764
883
  return res.status(400).json({ error: 'Path outside project' });
765
884
  }
766
885
  if (!existsSync(fullPath)) {
@@ -810,7 +929,7 @@ export function createApi(app, daemon) {
810
929
 
811
930
  // Read file contents
812
931
  app.get('/api/files/read', (req, res) => {
813
- const result = validateFilePath(req.query.path, daemon.projectDir);
932
+ const result = validateFilePath(req.query.path, getEditorRoot());
814
933
  if (result.error) return res.status(400).json({ error: result.error });
815
934
 
816
935
  if (!existsSync(result.fullPath)) {
@@ -846,7 +965,7 @@ export function createApi(app, daemon) {
846
965
  // Write file contents
847
966
  app.post('/api/files/write', (req, res) => {
848
967
  const { path: relPath, content } = req.body;
849
- const result = validateFilePath(relPath, daemon.projectDir);
968
+ const result = validateFilePath(relPath, getEditorRoot());
850
969
  if (result.error) return res.status(400).json({ error: result.error });
851
970
 
852
971
  if (typeof content !== 'string') {
@@ -868,7 +987,7 @@ export function createApi(app, daemon) {
868
987
  // Create a new file
869
988
  app.post('/api/files/create', (req, res) => {
870
989
  const { path: relPath, content = '' } = req.body;
871
- const result = validateFilePath(relPath, daemon.projectDir);
990
+ const result = validateFilePath(relPath, getEditorRoot());
872
991
  if (result.error) return res.status(400).json({ error: result.error });
873
992
 
874
993
  if (existsSync(result.fullPath)) {
@@ -893,7 +1012,7 @@ export function createApi(app, daemon) {
893
1012
  // Create a new directory
894
1013
  app.post('/api/files/mkdir', (req, res) => {
895
1014
  const { path: relPath } = req.body;
896
- const result = validateFilePath(relPath, daemon.projectDir);
1015
+ const result = validateFilePath(relPath, getEditorRoot());
897
1016
  if (result.error) return res.status(400).json({ error: result.error });
898
1017
 
899
1018
  if (existsSync(result.fullPath)) {
@@ -912,7 +1031,7 @@ export function createApi(app, daemon) {
912
1031
  // Delete a file or directory
913
1032
  app.delete('/api/files/delete', (req, res) => {
914
1033
  const relPath = req.query.path || req.body?.path;
915
- const result = validateFilePath(relPath, daemon.projectDir);
1034
+ const result = validateFilePath(relPath, getEditorRoot());
916
1035
  if (result.error) return res.status(400).json({ error: result.error });
917
1036
 
918
1037
  if (!existsSync(result.fullPath)) {
@@ -936,9 +1055,9 @@ export function createApi(app, daemon) {
936
1055
  // Rename / move a file or directory
937
1056
  app.post('/api/files/rename', (req, res) => {
938
1057
  const { oldPath, newPath } = req.body;
939
- const oldResult = validateFilePath(oldPath, daemon.projectDir);
1058
+ const oldResult = validateFilePath(oldPath, getEditorRoot());
940
1059
  if (oldResult.error) return res.status(400).json({ error: oldResult.error });
941
- const newResult = validateFilePath(newPath, daemon.projectDir);
1060
+ const newResult = validateFilePath(newPath, getEditorRoot());
942
1061
  if (newResult.error) return res.status(400).json({ error: newResult.error });
943
1062
 
944
1063
  if (!existsSync(oldResult.fullPath)) {
@@ -962,7 +1081,7 @@ export function createApi(app, daemon) {
962
1081
 
963
1082
  // Serve raw file (images, video, etc.)
964
1083
  app.get('/api/files/raw', (req, res) => {
965
- const result = validateFilePath(req.query.path, daemon.projectDir);
1084
+ const result = validateFilePath(req.query.path, getEditorRoot());
966
1085
  if (result.error) return res.status(400).json({ error: result.error });
967
1086
 
968
1087
  if (!existsSync(result.fullPath)) {
@@ -1033,9 +1152,25 @@ export function createApi(app, daemon) {
1033
1152
 
1034
1153
  // --- Recommended Team (from planner) ---
1035
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
+
1036
1171
  app.get('/api/recommended-team', (req, res) => {
1037
- const teamPath = resolve(daemon.grooveDir, 'recommended-team.json');
1038
- if (!existsSync(teamPath)) {
1172
+ const teamPath = findRecommendedTeam();
1173
+ if (!teamPath) {
1039
1174
  return res.json({ exists: false, agents: [] });
1040
1175
  }
1041
1176
  try {
@@ -1047,8 +1182,8 @@ export function createApi(app, daemon) {
1047
1182
  });
1048
1183
 
1049
1184
  app.post('/api/recommended-team/launch', async (req, res) => {
1050
- const teamPath = resolve(daemon.grooveDir, 'recommended-team.json');
1051
- if (!existsSync(teamPath)) {
1185
+ const teamPath = findRecommendedTeam();
1186
+ if (!teamPath) {
1052
1187
  return res.status(404).json({ error: 'No recommended team found. Run a planner first.' });
1053
1188
  }
1054
1189
  try {
@@ -1057,8 +1192,24 @@ export function createApi(app, daemon) {
1057
1192
  return res.status(400).json({ error: 'Recommended team is empty' });
1058
1193
  }
1059
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
1060
1210
  const spawned = [];
1061
- for (const config of agents) {
1211
+ const phase1Ids = [];
1212
+ for (const config of phase1) {
1062
1213
  const validated = validateAgentConfig({
1063
1214
  role: config.role,
1064
1215
  scope: config.scope || [],
@@ -1066,19 +1217,59 @@ export function createApi(app, daemon) {
1066
1217
  provider: config.provider || 'claude-code',
1067
1218
  model: config.model || 'auto',
1068
1219
  permission: config.permission || 'auto',
1069
- workingDir: config.workingDir || undefined,
1220
+ workingDir: config.workingDir || defaultDir,
1221
+ name: config.name || undefined,
1070
1222
  });
1071
1223
  const agent = await daemon.processes.spawn(validated);
1072
1224
  spawned.push({ id: agent.id, name: agent.name, role: agent.role });
1225
+ phase1Ids.push(agent.id);
1226
+ }
1227
+
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
+ });
1073
1241
  }
1074
1242
 
1075
- daemon.audit.log('team.launch', { count: spawned.length, agents: spawned.map((a) => a.role) });
1076
- res.json({ launched: spawned.length, agents: spawned });
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 });
1077
1248
  } catch (err) {
1078
1249
  res.status(500).json({ error: err.message });
1079
1250
  }
1080
1251
  });
1081
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
+
1082
1273
  // --- Command Center Dashboard ---
1083
1274
 
1084
1275
  app.get('/api/dashboard', (req, res) => {
@@ -1259,7 +1450,7 @@ export function createApi(app, daemon) {
1259
1450
  app.patch('/api/config', async (req, res) => {
1260
1451
  const ALLOWED_KEYS = [
1261
1452
  'port', 'journalistInterval', 'rotationThreshold', 'autoRotation',
1262
- 'qcThreshold', 'maxAgents', 'defaultProvider',
1453
+ 'qcThreshold', 'maxAgents', 'defaultProvider', 'defaultWorkingDir',
1263
1454
  ];
1264
1455
  for (const key of Object.keys(req.body)) {
1265
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();