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.
- package/CLAUDE.md +34 -2
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +124 -6
- package/node_modules/@groove-dev/daemon/src/introducer.js +7 -2
- package/node_modules/@groove-dev/daemon/src/process.js +11 -8
- package/node_modules/@groove-dev/gui/dist/assets/{codemirror-BYKpdS2W.js → codemirror-BQqYnZfL.js} +10 -10
- package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +8694 -0
- package/node_modules/@groove-dev/gui/dist/index.html +3 -3
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +87 -39
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +174 -70
- package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +199 -0
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +81 -4
- package/node_modules/@groove-dev/gui/src/components/editor/editor-toolbar.jsx +179 -0
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +111 -4
- package/node_modules/@groove-dev/gui/src/components/editor/inline-prompt.jsx +67 -0
- package/node_modules/@groove-dev/gui/src/components/editor/quick-search.jsx +170 -0
- package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +88 -0
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +1 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +5 -9
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +8 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +13 -8
- package/node_modules/@groove-dev/gui/src/stores/groove.js +70 -2
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +7 -7
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +219 -67
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +124 -6
- package/packages/daemon/src/introducer.js +7 -2
- package/packages/daemon/src/process.js +11 -8
- package/packages/gui/dist/assets/{codemirror-BYKpdS2W.js → codemirror-BQqYnZfL.js} +10 -10
- package/packages/gui/dist/assets/index-AkOtskHS.css +1 -0
- package/packages/gui/dist/assets/index-B4uYLR57.js +8694 -0
- package/packages/gui/dist/index.html +3 -3
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/code-review.jsx +87 -39
- package/packages/gui/src/components/agents/diff-viewer.jsx +174 -70
- package/packages/gui/src/components/editor/ai-panel.jsx +199 -0
- package/packages/gui/src/components/editor/code-editor.jsx +81 -4
- package/packages/gui/src/components/editor/editor-toolbar.jsx +179 -0
- package/packages/gui/src/components/editor/file-tree.jsx +111 -4
- package/packages/gui/src/components/editor/inline-prompt.jsx +67 -0
- package/packages/gui/src/components/editor/quick-search.jsx +170 -0
- package/packages/gui/src/components/editor/selection-menu.jsx +88 -0
- package/packages/gui/src/components/editor/terminal.jsx +1 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +5 -9
- package/packages/gui/src/components/layout/terminal-panel.jsx +8 -0
- package/packages/gui/src/components/ui/toast.jsx +13 -8
- package/packages/gui/src/stores/groove.js +70 -2
- package/packages/gui/src/views/agents.jsx +7 -7
- package/packages/gui/src/views/editor.jsx +219 -67
- package/node_modules/@groove-dev/gui/dist/assets/index-DcNgRadn.js +0 -8689
- package/node_modules/@groove-dev/gui/dist/assets/index-EY6WfKWH.css +0 -1
- package/packages/gui/dist/assets/index-DcNgRadn.js +0 -8689
- 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
|
-
-
|
|
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:
|
|
@@ -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,
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|