groove-dev 0.10.10 → 0.11.0
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/README.md +24 -16
- 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 +22 -0
- package/node_modules/@groove-dev/daemon/src/index.js +5 -0
- package/node_modules/@groove-dev/daemon/src/indexer.js +324 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +55 -4
- package/node_modules/@groove-dev/daemon/src/journalist.js +140 -51
- package/node_modules/@groove-dev/daemon/src/process.js +3 -2
- package/node_modules/@groove-dev/gui/dist/assets/index-BqZnnVJF.js +73 -0
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/AgentNode.jsx +6 -4
- package/node_modules/@groove-dev/gui/src/components/AgentStats.jsx +1 -0
- package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +70 -0
- 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 +22 -0
- package/packages/daemon/src/index.js +5 -0
- package/packages/daemon/src/indexer.js +324 -0
- package/packages/daemon/src/introducer.js +55 -4
- package/packages/daemon/src/journalist.js +140 -51
- package/packages/daemon/src/process.js +3 -2
- package/packages/gui/dist/assets/index-BqZnnVJF.js +73 -0
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/AgentNode.jsx +6 -4
- package/packages/gui/src/components/AgentStats.jsx +1 -0
- package/packages/gui/src/components/SpawnPanel.jsx +70 -0
- package/groove-logo.png +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BPVh7Oqk.js +0 -73
- package/packages/gui/dist/assets/index-BPVh7Oqk.js +0 -73
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>GROOVE</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-BqZnnVJF.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-CPzm9ZE9.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
@@ -35,10 +35,12 @@ export default function AgentNode({ data }) {
|
|
|
35
35
|
? data.tokensUsed > 999 ? `${(data.tokensUsed / 1000).toFixed(1)}k` : `${data.tokensUsed}`
|
|
36
36
|
: '0';
|
|
37
37
|
|
|
38
|
-
// Get scope summary for activity text
|
|
39
|
-
const activity = data.
|
|
40
|
-
? data.
|
|
41
|
-
: data.
|
|
38
|
+
// Get scope summary for activity text — prefer workingDir if set
|
|
39
|
+
const activity = data.workingDir
|
|
40
|
+
? data.workingDir.replace(/^\.\//, '')
|
|
41
|
+
: data.scope?.length > 0
|
|
42
|
+
? data.scope[0].replace(/\/\*\*$/, '').replace(/^src\//, '')
|
|
43
|
+
: data.role;
|
|
42
44
|
|
|
43
45
|
return (
|
|
44
46
|
<div style={{
|
|
@@ -61,6 +61,7 @@ export default function AgentStats({ agent }) {
|
|
|
61
61
|
<InfoRow label="Role" value={agent.role} />
|
|
62
62
|
<InfoRow label="Provider" value={agent.provider} />
|
|
63
63
|
<InfoRow label="Model" value={agent.model || 'default'} />
|
|
64
|
+
{agent.workingDir && <InfoRow label="Directory" value={agent.workingDir} />}
|
|
64
65
|
<InfoRow label="Scope" value={(agent.scope || []).join(', ') || 'unrestricted'} />
|
|
65
66
|
<InfoRow label="Spawned" value={agent.spawnedAt ? new Date(agent.spawnedAt).toLocaleTimeString() : '-'} />
|
|
66
67
|
<InfoRow label="Last Active" value={agent.lastActivity ? new Date(agent.lastActivity).toLocaleTimeString() : '-'} />
|
|
@@ -34,12 +34,15 @@ export default function SpawnPanel() {
|
|
|
34
34
|
const [submitting, setSubmitting] = useState(false);
|
|
35
35
|
const [error, setError] = useState('');
|
|
36
36
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
37
|
+
const [workingDir, setWorkingDir] = useState('');
|
|
38
|
+
const [workspaces, setWorkspaces] = useState([]);
|
|
37
39
|
const [connectingProvider, setConnectingProvider] = useState(null);
|
|
38
40
|
const [apiKeyInput, setApiKeyInput] = useState('');
|
|
39
41
|
const [keySaving, setKeySaving] = useState(false);
|
|
40
42
|
|
|
41
43
|
useEffect(() => {
|
|
42
44
|
fetchProviders();
|
|
45
|
+
fetchWorkspaces();
|
|
43
46
|
}, []);
|
|
44
47
|
|
|
45
48
|
async function fetchProviders() {
|
|
@@ -49,6 +52,14 @@ export default function SpawnPanel() {
|
|
|
49
52
|
} catch { /* ignore */ }
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
async function fetchWorkspaces() {
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch('/api/indexer/workspaces');
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
setWorkspaces(data.workspaces || []);
|
|
60
|
+
} catch { /* ignore */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
const selectedPreset = ROLE_PRESETS.find((p) => p.id === role);
|
|
53
64
|
const effectiveScope = role === 'custom'
|
|
54
65
|
? scope
|
|
@@ -113,6 +124,7 @@ export default function SpawnPanel() {
|
|
|
113
124
|
model: model || 'auto',
|
|
114
125
|
provider,
|
|
115
126
|
permission,
|
|
127
|
+
...(workingDir.trim() ? { workingDir: workingDir.trim() } : {}),
|
|
116
128
|
});
|
|
117
129
|
closeDetail();
|
|
118
130
|
} catch (err) {
|
|
@@ -198,6 +210,42 @@ export default function SpawnPanel() {
|
|
|
198
210
|
rows={3}
|
|
199
211
|
/>
|
|
200
212
|
|
|
213
|
+
{/* Workspace picker — visible by default when workspaces detected */}
|
|
214
|
+
{workspaces.length > 0 && (
|
|
215
|
+
<>
|
|
216
|
+
<div style={styles.label}>DIRECTORY</div>
|
|
217
|
+
<div style={styles.wsRow}>
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
onClick={() => setWorkingDir('')}
|
|
221
|
+
style={{
|
|
222
|
+
...styles.wsBtn,
|
|
223
|
+
...(!workingDir ? { borderColor: 'var(--accent)', color: 'var(--text-bright)' } : {}),
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
project root
|
|
227
|
+
</button>
|
|
228
|
+
{workspaces.map((ws) => (
|
|
229
|
+
<button
|
|
230
|
+
key={ws.path}
|
|
231
|
+
type="button"
|
|
232
|
+
onClick={() => setWorkingDir(ws.path)}
|
|
233
|
+
style={{
|
|
234
|
+
...styles.wsBtn,
|
|
235
|
+
...(workingDir === ws.path ? { borderColor: 'var(--accent)', color: 'var(--text-bright)' } : {}),
|
|
236
|
+
}}
|
|
237
|
+
title={`${ws.name} (${ws.files} files)`}
|
|
238
|
+
>
|
|
239
|
+
{ws.path}
|
|
240
|
+
</button>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
<div style={styles.hint}>
|
|
244
|
+
Agent spawns inside this directory and only sees this subtree
|
|
245
|
+
</div>
|
|
246
|
+
</>
|
|
247
|
+
)}
|
|
248
|
+
|
|
201
249
|
{/* Permissions */}
|
|
202
250
|
<div style={styles.label}>PERMISSIONS</div>
|
|
203
251
|
<div style={styles.permGrid}>
|
|
@@ -231,6 +279,18 @@ export default function SpawnPanel() {
|
|
|
231
279
|
|
|
232
280
|
{showAdvanced && (
|
|
233
281
|
<>
|
|
282
|
+
{/* Working directory — manual input for custom paths */}
|
|
283
|
+
<div style={styles.label}>WORKING DIRECTORY</div>
|
|
284
|
+
<input
|
|
285
|
+
style={styles.input}
|
|
286
|
+
placeholder="e.g. packages/frontend (default: project root)"
|
|
287
|
+
value={workingDir}
|
|
288
|
+
onChange={(e) => setWorkingDir(e.target.value)}
|
|
289
|
+
/>
|
|
290
|
+
<div style={styles.hint}>
|
|
291
|
+
Relative path — or use the directory buttons above
|
|
292
|
+
</div>
|
|
293
|
+
|
|
234
294
|
{/* Provider selector with connection flow */}
|
|
235
295
|
<div style={styles.label}>PROVIDER</div>
|
|
236
296
|
{providerList.map((p) => {
|
|
@@ -501,6 +561,16 @@ const styles = {
|
|
|
501
561
|
hint: {
|
|
502
562
|
fontSize: 10, color: 'var(--text-muted)', marginTop: 3,
|
|
503
563
|
},
|
|
564
|
+
wsRow: {
|
|
565
|
+
display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 6,
|
|
566
|
+
},
|
|
567
|
+
wsBtn: {
|
|
568
|
+
background: 'var(--bg-surface)', border: '1px solid var(--border)',
|
|
569
|
+
borderRadius: 2, padding: '3px 8px',
|
|
570
|
+
color: 'var(--text-dim)', fontSize: 10, cursor: 'pointer',
|
|
571
|
+
fontFamily: 'var(--font)',
|
|
572
|
+
transition: 'color 0.1s, border-color 0.1s',
|
|
573
|
+
},
|
|
504
574
|
error: {
|
|
505
575
|
color: 'var(--red)', fontSize: 11, marginTop: 8,
|
|
506
576
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Open-source agent orchestration layer for AI coding tools. GUI dashboard, multi-agent coordination, zero cold-start (Journalist), infinite sessions (adaptive context rotation), AI Project Manager, Quick Launch. Works with Claude Code, Codex, Gemini CLI, Ollama.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -341,6 +341,27 @@ export function createApi(app, daemon) {
|
|
|
341
341
|
res.json(daemon.adaptive.getAllProfiles());
|
|
342
342
|
});
|
|
343
343
|
|
|
344
|
+
// --- Codebase Indexer ---
|
|
345
|
+
|
|
346
|
+
app.get('/api/indexer', (req, res) => {
|
|
347
|
+
res.json(daemon.indexer.getStatus());
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
app.get('/api/indexer/workspaces', (req, res) => {
|
|
351
|
+
res.json({
|
|
352
|
+
workspaces: daemon.indexer.getWorkspaces(),
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
app.post('/api/indexer/rescan', (req, res) => {
|
|
357
|
+
try {
|
|
358
|
+
daemon.indexer.scan();
|
|
359
|
+
res.json({ ok: true, ...daemon.indexer.getStatus() });
|
|
360
|
+
} catch (err) {
|
|
361
|
+
res.status(500).json({ error: err.message });
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
344
365
|
// --- Project Manager (AI Review Gate) ---
|
|
345
366
|
|
|
346
367
|
// Agent knocks on PM before risky operations (Auto permission mode)
|
|
@@ -401,6 +422,7 @@ export function createApi(app, daemon) {
|
|
|
401
422
|
provider: config.provider || 'claude-code',
|
|
402
423
|
model: config.model || 'auto',
|
|
403
424
|
permission: config.permission || 'auto',
|
|
425
|
+
workingDir: config.workingDir || undefined,
|
|
404
426
|
});
|
|
405
427
|
const agent = await daemon.processes.spawn(validated);
|
|
406
428
|
spawned.push({ id: agent.id, name: agent.name, role: agent.role });
|
|
@@ -23,6 +23,7 @@ import { CredentialStore } from './credentials.js';
|
|
|
23
23
|
import { TaskClassifier } from './classifier.js';
|
|
24
24
|
import { ModelRouter } from './router.js';
|
|
25
25
|
import { ProjectManager } from './pm.js';
|
|
26
|
+
import { CodebaseIndexer } from './indexer.js';
|
|
26
27
|
import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
|
|
27
28
|
|
|
28
29
|
const DEFAULT_PORT = 31415;
|
|
@@ -69,6 +70,7 @@ export class Daemon {
|
|
|
69
70
|
this.classifier = new TaskClassifier();
|
|
70
71
|
this.router = new ModelRouter(this);
|
|
71
72
|
this.pm = new ProjectManager(this);
|
|
73
|
+
this.indexer = new CodebaseIndexer(this);
|
|
72
74
|
|
|
73
75
|
// HTTP + WebSocket server
|
|
74
76
|
this.app = express();
|
|
@@ -181,6 +183,9 @@ export class Daemon {
|
|
|
181
183
|
this.journalist.start();
|
|
182
184
|
this.rotator.start();
|
|
183
185
|
|
|
186
|
+
// Scan codebase for workspace/structure awareness
|
|
187
|
+
this.indexer.scan();
|
|
188
|
+
|
|
184
189
|
resolvePromise(this);
|
|
185
190
|
});
|
|
186
191
|
});
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// GROOVE — Codebase Indexer
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
//
|
|
4
|
+
// Scans the project structure on daemon start to detect monorepo workspaces,
|
|
5
|
+
// key files, and directory layout. Agents read this instead of spending
|
|
6
|
+
// thousands of tokens exploring the file tree.
|
|
7
|
+
|
|
8
|
+
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync } from 'fs';
|
|
9
|
+
import { resolve, relative, basename, join } from 'path';
|
|
10
|
+
|
|
11
|
+
const IGNORE_DIRS = new Set([
|
|
12
|
+
'node_modules', '.git', '.groove', 'dist', 'build', '.next', '.nuxt',
|
|
13
|
+
'.output', '.cache', '.turbo', '.vercel', '.svelte-kit', 'coverage',
|
|
14
|
+
'__pycache__', '.venv', 'venv', 'vendor', 'target', '.gradle',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const KEY_FILES = [
|
|
18
|
+
'package.json', 'tsconfig.json', 'README.md', 'CLAUDE.md',
|
|
19
|
+
'ARCHITECTURE.md', 'Cargo.toml', 'go.mod', 'pyproject.toml',
|
|
20
|
+
'Makefile', 'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const MAX_DEPTH = 4;
|
|
24
|
+
|
|
25
|
+
export class CodebaseIndexer {
|
|
26
|
+
constructor(daemon) {
|
|
27
|
+
this.daemon = daemon;
|
|
28
|
+
this.index = null; // { tree, workspaces, keyFiles, stats }
|
|
29
|
+
this.lastIndexTime = null;
|
|
30
|
+
this.indexPath = resolve(daemon.grooveDir, 'codebase-index.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Scan the project directory and build the codebase index.
|
|
35
|
+
* Called on daemon start. Fast — only reads directory entries, not file contents.
|
|
36
|
+
*/
|
|
37
|
+
scan() {
|
|
38
|
+
const rootDir = this.daemon.projectDir;
|
|
39
|
+
const tree = [];
|
|
40
|
+
const workspaces = [];
|
|
41
|
+
const keyFiles = [];
|
|
42
|
+
let totalFiles = 0;
|
|
43
|
+
let totalDirs = 0;
|
|
44
|
+
|
|
45
|
+
// Recursive directory walker (depth-limited)
|
|
46
|
+
const walk = (dir, depth, relPath) => {
|
|
47
|
+
if (depth > MAX_DEPTH) return;
|
|
48
|
+
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
52
|
+
} catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const dirs = [];
|
|
57
|
+
const files = [];
|
|
58
|
+
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (entry.name.startsWith('.') && entry.name !== '.github') continue;
|
|
61
|
+
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
64
|
+
dirs.push(entry.name);
|
|
65
|
+
totalDirs++;
|
|
66
|
+
} else if (entry.isFile()) {
|
|
67
|
+
files.push(entry.name);
|
|
68
|
+
totalFiles++;
|
|
69
|
+
|
|
70
|
+
// Track key files
|
|
71
|
+
if (KEY_FILES.includes(entry.name)) {
|
|
72
|
+
const filePath = relPath ? `${relPath}/${entry.name}` : entry.name;
|
|
73
|
+
keyFiles.push(filePath);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add to tree
|
|
79
|
+
const nodePath = relPath || '.';
|
|
80
|
+
tree.push({
|
|
81
|
+
path: nodePath,
|
|
82
|
+
depth,
|
|
83
|
+
dirs: dirs.length,
|
|
84
|
+
files: files.length,
|
|
85
|
+
children: dirs,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Recurse into subdirectories
|
|
89
|
+
for (const d of dirs) {
|
|
90
|
+
walk(resolve(dir, d), depth + 1, relPath ? `${relPath}/${d}` : d);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
walk(rootDir, 0, '');
|
|
95
|
+
|
|
96
|
+
// Detect workspaces
|
|
97
|
+
this.detectWorkspaces(rootDir, workspaces, tree);
|
|
98
|
+
|
|
99
|
+
this.index = {
|
|
100
|
+
projectName: basename(rootDir),
|
|
101
|
+
scannedAt: new Date().toISOString(),
|
|
102
|
+
stats: { totalFiles, totalDirs, treeDepth: MAX_DEPTH },
|
|
103
|
+
workspaces,
|
|
104
|
+
keyFiles,
|
|
105
|
+
tree,
|
|
106
|
+
};
|
|
107
|
+
this.lastIndexTime = Date.now();
|
|
108
|
+
|
|
109
|
+
// Persist to .groove/codebase-index.json
|
|
110
|
+
try {
|
|
111
|
+
writeFileSync(this.indexPath, JSON.stringify(this.index, null, 2));
|
|
112
|
+
} catch {
|
|
113
|
+
// Non-fatal — index is in memory either way
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Broadcast to GUI
|
|
117
|
+
this.daemon.broadcast({
|
|
118
|
+
type: 'indexer:complete',
|
|
119
|
+
data: {
|
|
120
|
+
workspaceCount: workspaces.length,
|
|
121
|
+
workspaces,
|
|
122
|
+
stats: this.index.stats,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return this.index;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Detect monorepo workspaces from common patterns.
|
|
131
|
+
*/
|
|
132
|
+
detectWorkspaces(rootDir, workspaces, tree) {
|
|
133
|
+
// 1. npm/yarn workspaces (package.json → workspaces field)
|
|
134
|
+
const rootPkg = this.readJson(resolve(rootDir, 'package.json'));
|
|
135
|
+
if (rootPkg?.workspaces) {
|
|
136
|
+
const patterns = Array.isArray(rootPkg.workspaces)
|
|
137
|
+
? rootPkg.workspaces
|
|
138
|
+
: rootPkg.workspaces.packages || [];
|
|
139
|
+
this.resolveWorkspacePatterns(rootDir, patterns, workspaces, 'npm-workspaces');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 2. pnpm workspaces
|
|
143
|
+
const pnpmPath = resolve(rootDir, 'pnpm-workspace.yaml');
|
|
144
|
+
if (existsSync(pnpmPath)) {
|
|
145
|
+
try {
|
|
146
|
+
const content = readFileSync(pnpmPath, 'utf8');
|
|
147
|
+
// Simple YAML parse — extract lines like " - packages/*"
|
|
148
|
+
const patterns = [];
|
|
149
|
+
for (const line of content.split('\n')) {
|
|
150
|
+
const match = line.match(/^\s*-\s+['"]?([^'"]+)['"]?\s*$/);
|
|
151
|
+
if (match) patterns.push(match[1]);
|
|
152
|
+
}
|
|
153
|
+
if (patterns.length > 0) {
|
|
154
|
+
this.resolveWorkspacePatterns(rootDir, patterns, workspaces, 'pnpm-workspaces');
|
|
155
|
+
}
|
|
156
|
+
} catch { /* ignore */ }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 3. lerna.json
|
|
160
|
+
const lernaPath = resolve(rootDir, 'lerna.json');
|
|
161
|
+
if (existsSync(lernaPath)) {
|
|
162
|
+
const lerna = this.readJson(lernaPath);
|
|
163
|
+
if (lerna?.packages) {
|
|
164
|
+
this.resolveWorkspacePatterns(rootDir, lerna.packages, workspaces, 'lerna');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 4. Fallback: multiple package.json at depth 1-2 (non-declared monorepo)
|
|
169
|
+
if (workspaces.length === 0) {
|
|
170
|
+
const subPkgs = tree.filter((node) =>
|
|
171
|
+
node.depth >= 1 && node.depth <= 2 &&
|
|
172
|
+
this.hasKeyFile(rootDir, node.path, 'package.json')
|
|
173
|
+
);
|
|
174
|
+
if (subPkgs.length >= 2) {
|
|
175
|
+
for (const node of subPkgs) {
|
|
176
|
+
const pkgJson = this.readJson(resolve(rootDir, node.path, 'package.json'));
|
|
177
|
+
workspaces.push({
|
|
178
|
+
path: node.path,
|
|
179
|
+
name: pkgJson?.name || basename(node.path),
|
|
180
|
+
type: 'detected',
|
|
181
|
+
files: node.files,
|
|
182
|
+
dirs: node.dirs,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resolve workspace glob patterns (e.g. "packages/*") into actual directories.
|
|
191
|
+
*/
|
|
192
|
+
resolveWorkspacePatterns(rootDir, patterns, workspaces, type) {
|
|
193
|
+
const seen = new Set(workspaces.map((w) => w.path));
|
|
194
|
+
|
|
195
|
+
for (const pattern of patterns) {
|
|
196
|
+
// Handle simple glob: "packages/*", "apps/*"
|
|
197
|
+
const clean = pattern.replace(/\/?\*+$/, '');
|
|
198
|
+
const parentDir = resolve(rootDir, clean);
|
|
199
|
+
|
|
200
|
+
if (!existsSync(parentDir)) continue;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const stat = statSync(parentDir);
|
|
204
|
+
|
|
205
|
+
if (pattern.includes('*')) {
|
|
206
|
+
// It's a glob — list children of the parent directory
|
|
207
|
+
const entries = readdirSync(parentDir, { withFileTypes: true });
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
if (!entry.isDirectory() || IGNORE_DIRS.has(entry.name)) continue;
|
|
210
|
+
const wsPath = clean ? `${clean}/${entry.name}` : entry.name;
|
|
211
|
+
if (seen.has(wsPath)) continue;
|
|
212
|
+
seen.add(wsPath);
|
|
213
|
+
|
|
214
|
+
const fullPath = resolve(rootDir, wsPath);
|
|
215
|
+
const pkgJson = this.readJson(resolve(fullPath, 'package.json'));
|
|
216
|
+
const info = this.dirInfo(fullPath);
|
|
217
|
+
workspaces.push({
|
|
218
|
+
path: wsPath,
|
|
219
|
+
name: pkgJson?.name || entry.name,
|
|
220
|
+
type,
|
|
221
|
+
files: info.files,
|
|
222
|
+
dirs: info.dirs,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
} else if (stat.isDirectory()) {
|
|
226
|
+
// Direct path (e.g. "shared")
|
|
227
|
+
if (seen.has(clean)) continue;
|
|
228
|
+
seen.add(clean);
|
|
229
|
+
|
|
230
|
+
const pkgJson = this.readJson(resolve(parentDir, 'package.json'));
|
|
231
|
+
const info = this.dirInfo(parentDir);
|
|
232
|
+
workspaces.push({
|
|
233
|
+
path: clean,
|
|
234
|
+
name: pkgJson?.name || basename(clean),
|
|
235
|
+
type,
|
|
236
|
+
files: info.files,
|
|
237
|
+
dirs: info.dirs,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
} catch { /* skip inaccessible dirs */ }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Helpers ──
|
|
245
|
+
|
|
246
|
+
readJson(filePath) {
|
|
247
|
+
try {
|
|
248
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
hasKeyFile(rootDir, relPath, filename) {
|
|
255
|
+
return existsSync(resolve(rootDir, relPath, filename));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
dirInfo(dirPath) {
|
|
259
|
+
try {
|
|
260
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
261
|
+
let files = 0, dirs = 0;
|
|
262
|
+
for (const e of entries) {
|
|
263
|
+
if (e.name.startsWith('.')) continue;
|
|
264
|
+
if (e.isFile()) files++;
|
|
265
|
+
else if (e.isDirectory() && !IGNORE_DIRS.has(e.name)) dirs++;
|
|
266
|
+
}
|
|
267
|
+
return { files, dirs };
|
|
268
|
+
} catch {
|
|
269
|
+
return { files: 0, dirs: 0 };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Public API ──
|
|
274
|
+
|
|
275
|
+
getWorkspaces() {
|
|
276
|
+
return this.index?.workspaces || [];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
getIndex() {
|
|
280
|
+
return this.index;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Get a compact structural summary for agent context injection.
|
|
285
|
+
* Returns a markdown string with workspace layout and key files.
|
|
286
|
+
*/
|
|
287
|
+
getStructureSummary() {
|
|
288
|
+
if (!this.index) return null;
|
|
289
|
+
const { workspaces, keyFiles, stats, projectName } = this.index;
|
|
290
|
+
|
|
291
|
+
const lines = [];
|
|
292
|
+
lines.push(`Project: **${projectName}** (${stats.totalFiles} files, ${stats.totalDirs} directories)`);
|
|
293
|
+
|
|
294
|
+
if (workspaces.length > 0) {
|
|
295
|
+
lines.push('');
|
|
296
|
+
lines.push(`Workspaces (${workspaces.length}):`);
|
|
297
|
+
for (const ws of workspaces) {
|
|
298
|
+
lines.push(`- \`${ws.path}/\` — ${ws.name} (${ws.files} files, ${ws.dirs} subdirs)`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (keyFiles.length > 0) {
|
|
303
|
+
lines.push('');
|
|
304
|
+
lines.push('Key files:');
|
|
305
|
+
for (const f of keyFiles.slice(0, 30)) {
|
|
306
|
+
lines.push(`- ${f}`);
|
|
307
|
+
}
|
|
308
|
+
if (keyFiles.length > 30) {
|
|
309
|
+
lines.push(`- *(+${keyFiles.length - 30} more)*`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return lines.join('\n');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
getStatus() {
|
|
317
|
+
return {
|
|
318
|
+
indexed: !!this.index,
|
|
319
|
+
lastIndexTime: this.lastIndexTime,
|
|
320
|
+
workspaceCount: this.index?.workspaces?.length || 0,
|
|
321
|
+
stats: this.index?.stats || null,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -26,6 +26,10 @@ export class Introducer {
|
|
|
26
26
|
`You are **${newAgent.name}** (role: ${newAgent.role}), managed by GROOVE.`,
|
|
27
27
|
];
|
|
28
28
|
|
|
29
|
+
if (newAgent.workingDir) {
|
|
30
|
+
lines.push(`Your working directory: \`${newAgent.workingDir}\` — you are spawned inside this subdirectory. Stay within it unless coordination requires otherwise.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
if (newAgent.scope && newAgent.scope.length > 0) {
|
|
30
34
|
lines.push(`Your file scope: \`${newAgent.scope.join('`, `')}\``);
|
|
31
35
|
} else {
|
|
@@ -45,7 +49,8 @@ export class Introducer {
|
|
|
45
49
|
|
|
46
50
|
for (const other of others) {
|
|
47
51
|
const scope = other.scope?.length > 0 ? other.scope.join(', ') : 'unrestricted';
|
|
48
|
-
|
|
52
|
+
const dir = other.workingDir ? ` — dir: ${other.workingDir}` : '';
|
|
53
|
+
lines.push(`- **${other.name}** (${other.role}) — scope: ${scope}${dir} — ${other.status}`);
|
|
49
54
|
|
|
50
55
|
// Get files this agent created/modified
|
|
51
56
|
const files = this.daemon.journalist?.getAgentFiles(other) || [];
|
|
@@ -138,9 +143,54 @@ export class Introducer {
|
|
|
138
143
|
lines.push(`Read GROOVE_PROJECT_MAP.md for current project context from The Journalist.`);
|
|
139
144
|
}
|
|
140
145
|
|
|
146
|
+
// Codebase structure injection — give agents instant orientation
|
|
147
|
+
const structureSummary = this.daemon.indexer?.getStructureSummary();
|
|
148
|
+
if (structureSummary) {
|
|
149
|
+
lines.push('');
|
|
150
|
+
lines.push(`## Codebase Structure (auto-indexed)`);
|
|
151
|
+
lines.push('');
|
|
152
|
+
lines.push(structureSummary);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Architecture injection — auto-detect architecture docs and inject
|
|
156
|
+
// so every agent understands the big picture without spending tokens exploring
|
|
157
|
+
const archContent = this.loadArchitectureDoc();
|
|
158
|
+
if (archContent) {
|
|
159
|
+
lines.push('');
|
|
160
|
+
lines.push(`## Architecture (auto-injected)`);
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push(archContent);
|
|
163
|
+
}
|
|
164
|
+
|
|
141
165
|
return lines.join('\n');
|
|
142
166
|
}
|
|
143
167
|
|
|
168
|
+
loadArchitectureDoc() {
|
|
169
|
+
const projectDir = this.daemon.projectDir;
|
|
170
|
+
const candidates = [
|
|
171
|
+
'ARCHITECTURE.md',
|
|
172
|
+
'docs/architecture.md',
|
|
173
|
+
'.github/ARCHITECTURE.md',
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
for (const candidate of candidates) {
|
|
177
|
+
const fullPath = resolve(projectDir, candidate);
|
|
178
|
+
if (existsSync(fullPath)) {
|
|
179
|
+
try {
|
|
180
|
+
let content = readFileSync(fullPath, 'utf8').trim();
|
|
181
|
+
// Truncate to ~5K chars to keep context budget reasonable
|
|
182
|
+
if (content.length > 5000) {
|
|
183
|
+
content = content.slice(0, 5000) + '\n\n*(truncated — read full file for details)*';
|
|
184
|
+
}
|
|
185
|
+
return content;
|
|
186
|
+
} catch {
|
|
187
|
+
// ignore read errors
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
144
194
|
writeRegistryFile(projectDir) {
|
|
145
195
|
const agents = this.daemon.registry.getAll();
|
|
146
196
|
|
|
@@ -158,13 +208,14 @@ export class Introducer {
|
|
|
158
208
|
``,
|
|
159
209
|
`*Auto-generated by GROOVE. Do not edit manually.*`,
|
|
160
210
|
``,
|
|
161
|
-
`| ID | Name | Role | Provider | Scope | Status |`,
|
|
162
|
-
|
|
211
|
+
`| ID | Name | Role | Provider | Directory | Scope | Status |`,
|
|
212
|
+
`|----|------|------|----------|-----------|-------|--------|`,
|
|
163
213
|
];
|
|
164
214
|
|
|
165
215
|
for (const a of agents) {
|
|
166
216
|
const scope = a.scope?.length > 0 ? `\`${a.scope.join('`, `')}\`` : '-';
|
|
167
|
-
|
|
217
|
+
const dir = a.workingDir ? escapeMd(a.workingDir) : '-';
|
|
218
|
+
lines.push(`| ${escapeMd(a.id)} | ${escapeMd(a.name)} | ${escapeMd(a.role)} | ${escapeMd(a.provider)} | ${dir} | ${scope} | ${escapeMd(a.status)} |`);
|
|
168
219
|
}
|
|
169
220
|
|
|
170
221
|
lines.push('');
|