groove-dev 0.10.9 → 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/daemon/src/providers/gemini.js +5 -3
- 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/daemon/src/providers/gemini.js +5 -3
- 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
package/README.md
CHANGED
|
@@ -45,6 +45,15 @@ Agents in Auto mode knock on the PM before risky operations (creating files, del
|
|
|
45
45
|
|
|
46
46
|
Spawn a planner, describe your project. The planner writes a detailed plan and recommends a team. Click **Launch Team** — all agents spawn with proper roles, scopes, and prompts. One click from idea to a full team building your app.
|
|
47
47
|
|
|
48
|
+
### Workspaces (Large Codebase Support)
|
|
49
|
+
|
|
50
|
+
GROOVE auto-detects monorepo workspaces (npm, pnpm, lerna) and lets you spawn each agent in its own subdirectory. A frontend agent only sees `packages/frontend/`. A backend agent only sees `packages/backend/`. No wasted context on irrelevant code.
|
|
51
|
+
|
|
52
|
+
- **Codebase indexer** — scans project structure on start, gives every agent instant orientation
|
|
53
|
+
- **Architecture injection** — auto-detects `ARCHITECTURE.md` and injects it into every agent's context
|
|
54
|
+
- **Per-workspace journalist** — synthesis and handoff briefs scoped to each agent's directory
|
|
55
|
+
- **Quick-pick** — detected workspaces appear as buttons in the spawn panel
|
|
56
|
+
|
|
48
57
|
### Multi-Agent Coordination
|
|
49
58
|
|
|
50
59
|
- **Introduction protocol** — every agent knows its teammates, their files, and their work
|
|
@@ -56,10 +65,9 @@ Spawn a planner, describe your project. The planner writes a detailed plan and r
|
|
|
56
65
|
|
|
57
66
|
| Provider | Auth | Models |
|
|
58
67
|
|----------|------|--------|
|
|
59
|
-
| **Claude Code** | Subscription | Opus, Sonnet, Haiku |
|
|
60
|
-
| **Codex** | API Key | o3, o4-mini |
|
|
61
|
-
| **Gemini CLI** | API Key | 2.5 Pro, 2.5 Flash |
|
|
62
|
-
|
|
68
|
+
| **Claude Code** | Subscription | Opus 4.6, Sonnet 4.6, Haiku 4.5 |
|
|
69
|
+
| **Codex** | API Key | o3, o4-mini, GPT-4.1, GPT-4.1 Mini, GPT-4.1 Nano |
|
|
70
|
+
| **Gemini CLI** | API Key | 3.1 Pro, 3 Flash, 3.1 Flash Lite, 2.5 Pro, 2.5 Flash |
|
|
63
71
|
| **Ollama** | Local | Any |
|
|
64
72
|
|
|
65
73
|
GROOVE is a process manager — it spawns actual AI tool binaries. It never proxies API calls, never touches OAuth tokens, never impersonates any client. Your AI tools talk directly to their servers.
|
|
@@ -90,19 +98,19 @@ GROOVE routes tasks to the cheapest model that can handle them. Planners get Opu
|
|
|
90
98
|
## Architecture
|
|
91
99
|
|
|
92
100
|
```
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
┌──────────────────────────────────────────────┐
|
|
102
|
+
│ GROOVE DAEMON (:31415) │
|
|
103
|
+
│ │
|
|
104
|
+
│ Registry · Introducer · Lock Manager │
|
|
105
|
+
│ Journalist · Rotator · Adaptive · Indexer │
|
|
106
|
+
│ Classifier · Router · PM · Teams │
|
|
107
|
+
│ │
|
|
108
|
+
│ REST API · WebSocket · GUI Server │
|
|
109
|
+
└──────────────────────┬───────────────────────┘
|
|
102
110
|
│
|
|
103
|
-
|
|
104
|
-
│
|
|
105
|
-
|
|
111
|
+
┌──────────────────────▼───────────────────────┐
|
|
112
|
+
│ Claude Code · Codex · Gemini CLI · Ollama │
|
|
113
|
+
└──────────────────────────────────────────────┘
|
|
106
114
|
```
|
|
107
115
|
|
|
108
116
|
## Links
|
|
@@ -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('');
|