groove-dev 0.27.138 → 0.27.139

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 (58) hide show
  1. package/CLAUDE.md +34 -2
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +124 -6
  5. package/node_modules/@groove-dev/daemon/src/introducer.js +7 -2
  6. package/node_modules/@groove-dev/daemon/src/process.js +11 -8
  7. package/node_modules/@groove-dev/gui/dist/assets/{codemirror-BYKpdS2W.js → codemirror-BQqYnZfL.js} +10 -10
  8. package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +1 -0
  9. package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +8694 -0
  10. package/node_modules/@groove-dev/gui/dist/index.html +3 -3
  11. package/node_modules/@groove-dev/gui/package.json +1 -1
  12. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +87 -39
  13. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +174 -70
  14. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +199 -0
  15. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +81 -4
  16. package/node_modules/@groove-dev/gui/src/components/editor/editor-toolbar.jsx +179 -0
  17. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +111 -4
  18. package/node_modules/@groove-dev/gui/src/components/editor/inline-prompt.jsx +67 -0
  19. package/node_modules/@groove-dev/gui/src/components/editor/quick-search.jsx +170 -0
  20. package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +88 -0
  21. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +1 -0
  22. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +5 -9
  23. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +8 -0
  24. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +13 -8
  25. package/node_modules/@groove-dev/gui/src/stores/groove.js +70 -2
  26. package/node_modules/@groove-dev/gui/src/views/agents.jsx +7 -7
  27. package/node_modules/@groove-dev/gui/src/views/editor.jsx +219 -67
  28. package/package.json +1 -1
  29. package/packages/cli/package.json +1 -1
  30. package/packages/daemon/package.json +1 -1
  31. package/packages/daemon/src/api.js +124 -6
  32. package/packages/daemon/src/introducer.js +7 -2
  33. package/packages/daemon/src/process.js +11 -8
  34. package/packages/gui/dist/assets/{codemirror-BYKpdS2W.js → codemirror-BQqYnZfL.js} +10 -10
  35. package/packages/gui/dist/assets/index-AkOtskHS.css +1 -0
  36. package/packages/gui/dist/assets/index-B4uYLR57.js +8694 -0
  37. package/packages/gui/dist/index.html +3 -3
  38. package/packages/gui/package.json +1 -1
  39. package/packages/gui/src/components/agents/code-review.jsx +87 -39
  40. package/packages/gui/src/components/agents/diff-viewer.jsx +174 -70
  41. package/packages/gui/src/components/editor/ai-panel.jsx +199 -0
  42. package/packages/gui/src/components/editor/code-editor.jsx +81 -4
  43. package/packages/gui/src/components/editor/editor-toolbar.jsx +179 -0
  44. package/packages/gui/src/components/editor/file-tree.jsx +111 -4
  45. package/packages/gui/src/components/editor/inline-prompt.jsx +67 -0
  46. package/packages/gui/src/components/editor/quick-search.jsx +170 -0
  47. package/packages/gui/src/components/editor/selection-menu.jsx +88 -0
  48. package/packages/gui/src/components/editor/terminal.jsx +1 -0
  49. package/packages/gui/src/components/layout/activity-bar.jsx +5 -9
  50. package/packages/gui/src/components/layout/terminal-panel.jsx +8 -0
  51. package/packages/gui/src/components/ui/toast.jsx +13 -8
  52. package/packages/gui/src/stores/groove.js +70 -2
  53. package/packages/gui/src/views/agents.jsx +7 -7
  54. package/packages/gui/src/views/editor.jsx +219 -67
  55. package/node_modules/@groove-dev/gui/dist/assets/index-DcNgRadn.js +0 -8689
  56. package/node_modules/@groove-dev/gui/dist/assets/index-EY6WfKWH.css +0 -1
  57. package/packages/gui/dist/assets/index-DcNgRadn.js +0 -8689
  58. package/packages/gui/dist/assets/index-EY6WfKWH.css +0 -1
package/CLAUDE.md CHANGED
@@ -1,7 +1,30 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
1
5
  # GROOVE — Agent Orchestration Layer
2
6
 
3
7
  > Spawn fast. Stay aware. Never lose context.
4
8
 
9
+ ## Build & Development Commands
10
+
11
+ ```bash
12
+ # Build GUI (required after any GUI changes — daemon serves built assets)
13
+ npm run build # or: cd packages/gui && npx vite build
14
+
15
+ # Run all tests (265 tests, 24 suites)
16
+ npm test # or: node --test packages/daemon/test/*.test.js
17
+
18
+ # Run a single test file
19
+ node --test packages/daemon/test/registry.test.js
20
+
21
+ # Dev servers (not for agents — agents use npm run build)
22
+ npm run dev:gui # Vite dev server (HMR)
23
+ npm run dev:daemon # Daemon in dev mode
24
+ ```
25
+
26
+ **Important:** GUI changes require `npm run build` to take effect (daemon serves static build from `packages/gui/dist/`). Daemon changes require a manual daemon restart — agents must NEVER restart the daemon themselves.
27
+
5
28
  ## What This Is
6
29
 
7
30
  GROOVE is a lightweight, open-source agent orchestration layer for AI coding tools. It is a **process manager** — it spawns, coordinates, and monitors AI coding agents. It does NOT wrap, proxy, or impersonate any AI API.
@@ -92,7 +115,7 @@ Bundled starter teams: `fullstack.json`, `api-builder.json`, `monorepo.json`
92
115
  - Tailwind CSS v4 + Radix UI (GUI styling + accessible primitives)
93
116
  - Zustand 5 (GUI state + WebSocket sync)
94
117
  - React Flow / @xyflow/react (agent tree visualization)
95
- - CodeMirror 6 (code editor, 7 languages)
118
+ - CodeMirror 6 + @uiw/codemirror-themes-all (code editor, 7 languages, 38 selectable themes)
96
119
  - xterm.js (terminal emulation)
97
120
  - Framer Motion (animations)
98
121
  - Lucide React (icons)
@@ -201,7 +224,7 @@ React app served by daemon at `http://localhost:31415`. VS Code-style layout. Ta
201
224
  - Log files created with 0o600 permissions
202
225
  - Command injection prevention (execFileSync with array args in tmux)
203
226
  - Scope patterns validated (no absolute paths, no traversal)
204
- - 137 automated tests across 14 suites
227
+ - 265 automated tests across 24 suites
205
228
 
206
229
  ## Conventions
207
230
 
@@ -211,6 +234,15 @@ React app served by daemon at `http://localhost:31415`. VS Code-style layout. Ta
211
234
  - `.groove/` directory in project root for runtime state (gitignored)
212
235
  - Generated markdown files: `AGENTS_REGISTRY.md`, `GROOVE_PROJECT_MAP.md`, `GROOVE_DECISIONS.md`
213
236
 
237
+ ## GUI Styling Rules
238
+
239
+ - **Tailwind CSS v4 only** — zero inline styles (no `style={{}}` except dynamic values like `width`, `height`)
240
+ - **No absolute-positioned floating buttons** over content — use flex layout rails instead
241
+ - **File tree icons** must be neutral/muted (`text-text-2`/`text-text-3`) — no rainbow colors
242
+ - **Editor themes** are user-selectable via status bar picker — do not hardcode syntax highlight colors
243
+ - **Font sizes:** file trees use `text-xs` (12px), editor uses 12px via theme
244
+ - **No inline styles for colors** — use design token CSS variables (`--color-accent`, `--color-danger`, etc.)
245
+
214
246
  ## Compliance (CRITICAL)
215
247
 
216
248
  GROOVE is a process manager, NOT a harness. Hard rules:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.138",
3
+ "version": "0.27.139",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.138",
3
+ "version": "0.27.139",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1693,18 +1693,29 @@ export function createApi(app, daemon) {
1693
1693
  // Rotation = full handoff brief (only for degradation or no session)
1694
1694
  app.post('/api/agents/:id/instruct', async (req, res) => {
1695
1695
  try {
1696
- const { message } = req.body;
1696
+ const { message, codeContext } = req.body;
1697
1697
  if (!message || typeof message !== 'string' || !message.trim()) {
1698
1698
  return res.status(400).json({ error: 'message is required' });
1699
1699
  }
1700
1700
  const agent = daemon.registry.get(req.params.id);
1701
1701
  if (!agent) return res.status(404).json({ error: 'Agent not found' });
1702
1702
 
1703
+ // Build the final instruction, optionally enriched with code context
1704
+ let finalMessage = message.trim();
1705
+ if (codeContext && typeof codeContext === 'object') {
1706
+ const { filePath, lineStart, lineEnd, selectedCode } = codeContext;
1707
+ if (filePath && typeof filePath === 'string' && selectedCode && typeof selectedCode === 'string') {
1708
+ const start = Number.isFinite(lineStart) ? lineStart : '?';
1709
+ const end = Number.isFinite(lineEnd) ? lineEnd : '?';
1710
+ finalMessage = `${finalMessage}\n\nCode context from ${filePath} (lines ${start}-${end}):\n\`\`\`\n${selectedCode}\n\`\`\``;
1711
+ }
1712
+ }
1713
+
1703
1714
  // Record user feedback so the journalist can include it in future agent context
1704
- if (daemon.journalist) daemon.journalist.recordUserFeedback(agent, message.trim());
1715
+ if (daemon.journalist) daemon.journalist.recordUserFeedback(agent, finalMessage);
1705
1716
 
1706
1717
  // Agent loop path — send message directly to the running loop
1707
- const wrappedMessage = wrapWithRoleReminder(agent.role, message.trim());
1718
+ const wrappedMessage = wrapWithRoleReminder(agent.role, finalMessage);
1708
1719
  if (daemon.processes.hasAgentLoop(req.params.id)) {
1709
1720
  const sent = await daemon.processes.sendMessage(req.params.id, wrappedMessage);
1710
1721
  if (sent) {
@@ -1731,7 +1742,7 @@ export function createApi(app, daemon) {
1731
1742
  scope: oldConfig.scope,
1732
1743
  provider: oldConfig.provider,
1733
1744
  model: oldConfig.model,
1734
- prompt: message.trim(),
1745
+ prompt: finalMessage,
1735
1746
  permission: oldConfig.permission || 'full',
1736
1747
  workingDir: oldConfig.workingDir,
1737
1748
  name: oldConfig.name,
@@ -1754,7 +1765,7 @@ export function createApi(app, daemon) {
1754
1765
  scope: oldConfig.scope,
1755
1766
  provider: oldConfig.provider,
1756
1767
  model: oldConfig.model,
1757
- prompt: message.trim(),
1768
+ prompt: finalMessage,
1758
1769
  introContext: oldConfig.introContext,
1759
1770
  permission: oldConfig.permission || 'full',
1760
1771
  workingDir: oldConfig.workingDir,
@@ -3340,11 +3351,118 @@ Keep responses concise. Help them think, don't lecture them about the system the
3340
3351
  });
3341
3352
  });
3342
3353
 
3354
+ // Git line status — per-line modification status for editor gutter decorations
3355
+ app.get('/api/files/git-line-status', (req, res) => {
3356
+ const relPath = req.query.path;
3357
+ if (!relPath || typeof relPath !== 'string') {
3358
+ return res.status(400).json({ error: 'path parameter is required' });
3359
+ }
3360
+ if (relPath.includes('\0') || relPath.startsWith('/')) {
3361
+ return res.status(400).json({ error: 'Invalid path' });
3362
+ }
3363
+ const segments = relPath.split(/[/\\]/);
3364
+ if (segments.some(s => s === '..')) {
3365
+ return res.status(400).json({ error: 'Path traversal not allowed' });
3366
+ }
3367
+
3368
+ const rootDir = getEditorRoot();
3369
+ if (!rootDir) return res.status(400).json({ error: 'Editor root not set' });
3370
+
3371
+ const fullPath = resolve(rootDir, relPath);
3372
+ if (!fullPath.startsWith(rootDir + sep) && fullPath !== rootDir) {
3373
+ return res.status(400).json({ error: 'Path outside project' });
3374
+ }
3375
+
3376
+ const result = { lines: { added: [], modified: [], deleted: [] } };
3377
+
3378
+ // Check if file is tracked by git
3379
+ try {
3380
+ execFileSync('git', ['ls-files', '--error-unmatch', '--', relPath], { cwd: rootDir, timeout: 5000, stdio: 'pipe' });
3381
+ } catch {
3382
+ // File not tracked — check if it exists (untracked = all lines added)
3383
+ if (existsSync(fullPath)) {
3384
+ try {
3385
+ const content = readFileSync(fullPath, 'utf8');
3386
+ const lineCount = content.split('\n').length;
3387
+ for (let i = 1; i <= lineCount; i++) result.lines.added.push(i);
3388
+ } catch { /* binary or unreadable */ }
3389
+ }
3390
+ return res.json(result);
3391
+ }
3392
+
3393
+ try {
3394
+ const diffOut = execFileSync('git', ['diff', '--unified=0', '--', relPath], {
3395
+ cwd: rootDir, timeout: 10000, maxBuffer: 5 * 1024 * 1024,
3396
+ }).toString();
3397
+
3398
+ if (!diffOut.trim()) return res.json(result);
3399
+
3400
+ // Check for binary
3401
+ if (diffOut.includes('Binary files')) return res.json(result);
3402
+
3403
+ // Parse unified diff hunks: @@ -oldStart,oldCount +newStart,newCount @@
3404
+ const hunkRe = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/gm;
3405
+ let match;
3406
+ while ((match = hunkRe.exec(diffOut)) !== null) {
3407
+ const oldCount = parseInt(match[2] ?? '1', 10);
3408
+ const newStart = parseInt(match[3], 10);
3409
+ const newCount = parseInt(match[4] ?? '1', 10);
3410
+
3411
+ if (oldCount === 0 && newCount > 0) {
3412
+ // Pure addition
3413
+ for (let i = newStart; i < newStart + newCount; i++) result.lines.added.push(i);
3414
+ } else if (newCount === 0 && oldCount > 0) {
3415
+ // Pure deletion — mark the line where content was removed
3416
+ result.lines.deleted.push(newStart);
3417
+ } else {
3418
+ // Modification
3419
+ for (let i = newStart; i < newStart + newCount; i++) result.lines.modified.push(i);
3420
+ }
3421
+ }
3422
+
3423
+ res.json(result);
3424
+ } catch (err) {
3425
+ if (err.status !== undefined) return res.json(result);
3426
+ res.status(500).json({ error: 'Failed to compute line status' });
3427
+ }
3428
+ });
3429
+
3430
+ // Git branches — list all local branches with current branch marked
3431
+ app.get('/api/files/git-branches', (req, res) => {
3432
+ const rootDir = getEditorRoot();
3433
+ if (!rootDir) return res.status(400).json({ error: 'Editor root not set' });
3434
+
3435
+ const fallback = { current: null, branches: [] };
3436
+
3437
+ try {
3438
+ let current = null;
3439
+ try {
3440
+ current = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
3441
+ cwd: rootDir, timeout: 5000, stdio: 'pipe',
3442
+ }).toString().trim();
3443
+ } catch { return res.json(fallback); }
3444
+
3445
+ const branchOut = execFileSync('git', ['branch', '--list', '--format=%(refname:short)'], {
3446
+ cwd: rootDir, timeout: 5000, stdio: 'pipe',
3447
+ }).toString();
3448
+
3449
+ const branches = branchOut.split('\n').map(b => b.trim()).filter(Boolean);
3450
+ res.json({ current, branches });
3451
+ } catch {
3452
+ res.json(fallback);
3453
+ }
3454
+ });
3455
+
3343
3456
  // Files touched by an agent during its session
3344
3457
  app.get('/api/agents/:id/files-touched', (req, res) => {
3345
3458
  const agent = daemon.registry.get(req.params.id);
3346
3459
  if (!agent) return res.status(404).json({ error: 'Agent not found' });
3347
- const files = daemon.registry.getFilesTouched(req.params.id);
3460
+ const rawFiles = daemon.registry.getFilesTouched(req.params.id);
3461
+ const rootDir = agent.workingDir || daemon.projectDir;
3462
+ const files = rawFiles.map(f => {
3463
+ const fullPath = isAbsolute(f.path) ? f.path : resolve(rootDir, f.path);
3464
+ return { ...f, exists: existsSync(fullPath) };
3465
+ });
3348
3466
  res.json({ files, total: files.length });
3349
3467
  });
3350
3468
 
@@ -173,12 +173,17 @@ export class Introducer {
173
173
  }
174
174
  }
175
175
 
176
- // Project files section — tell the new agent what exists and what to read
176
+ // Project files section — tell the new agent what exists
177
+ // When no task is assigned, list files as reference only (not an action prompt)
177
178
  if (allTeamFiles.length > 0) {
178
179
  lines.push('');
179
180
  lines.push(`## Project Files`);
180
181
  lines.push('');
181
- lines.push(`Your team has created the following files. **Read relevant ones before starting work** to understand what's been built and planned:`);
182
+ if (hasTask || isRotation) {
183
+ lines.push(`Your team has created the following files. **Read relevant ones before starting work** to understand what's been built and planned:`);
184
+ } else {
185
+ lines.push(`Your team has created the following files (for reference — do NOT read or act on these until you receive a task):`);
186
+ }
182
187
  lines.push('');
183
188
 
184
189
  // Group by agent for clarity
@@ -887,23 +887,26 @@ export class ProcessManager {
887
887
  }
888
888
  }
889
889
 
890
+ // Compute hasTask from actual prompt content — agents spawned without a
891
+ // prompt should NOT receive handoff history (prevents cross-team contamination).
892
+ // Discoveries + constraints are always injected (project knowledge).
893
+ // Handoffs are injected only when the agent has a real task or is a rotation.
894
+ const hasTask = !!(config.prompt && config.prompt.trim().length > 0);
895
+ const isRotation = !!(config.isRotation);
896
+
890
897
  // Pre-spawn task negotiation — if same-role agents are running,
891
- // query them about current work so the new agent gets a clear assignment
898
+ // query them about current work so the new agent gets a clear assignment.
899
+ // Only negotiate when the agent has a task — otherwise the negotiator
900
+ // output acts as an implicit task and the agent starts working immediately.
892
901
  const sameRole = registry.getAll().filter(
893
902
  (a) => a.role === config.role && a.id !== agent.id &&
894
903
  (a.status === 'running' || a.status === 'starting')
895
904
  );
896
905
  let taskNegotiation = '';
897
- if (sameRole.length > 0) {
906
+ if (sameRole.length > 0 && (hasTask || isRotation)) {
898
907
  taskNegotiation = await this.negotiateTaskSplit(agent, sameRole);
899
908
  }
900
909
 
901
- // Compute hasTask from actual prompt content — agents spawned without a
902
- // prompt should NOT receive handoff history (prevents cross-team contamination).
903
- // Discoveries + constraints are always injected (project knowledge).
904
- // Handoffs are injected only when the agent has a real task or is a rotation.
905
- const hasTask = !!(config.prompt && config.prompt.trim().length > 0);
906
- const isRotation = !!(config.isRotation);
907
910
  let introContext = introducer.generateContext(agent, { taskNegotiation, hasTask, isRotation });
908
911
 
909
912
  // Intro context size warning and optional truncation (Change 7)