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.
Files changed (35) hide show
  1. package/README.md +24 -16
  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 +22 -0
  5. package/node_modules/@groove-dev/daemon/src/index.js +5 -0
  6. package/node_modules/@groove-dev/daemon/src/indexer.js +324 -0
  7. package/node_modules/@groove-dev/daemon/src/introducer.js +55 -4
  8. package/node_modules/@groove-dev/daemon/src/journalist.js +140 -51
  9. package/node_modules/@groove-dev/daemon/src/process.js +3 -2
  10. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +5 -3
  11. package/node_modules/@groove-dev/gui/dist/assets/index-BqZnnVJF.js +73 -0
  12. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  13. package/node_modules/@groove-dev/gui/package.json +1 -1
  14. package/node_modules/@groove-dev/gui/src/components/AgentNode.jsx +6 -4
  15. package/node_modules/@groove-dev/gui/src/components/AgentStats.jsx +1 -0
  16. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +70 -0
  17. package/package.json +1 -1
  18. package/packages/cli/package.json +1 -1
  19. package/packages/daemon/package.json +1 -1
  20. package/packages/daemon/src/api.js +22 -0
  21. package/packages/daemon/src/index.js +5 -0
  22. package/packages/daemon/src/indexer.js +324 -0
  23. package/packages/daemon/src/introducer.js +55 -4
  24. package/packages/daemon/src/journalist.js +140 -51
  25. package/packages/daemon/src/process.js +3 -2
  26. package/packages/daemon/src/providers/gemini.js +5 -3
  27. package/packages/gui/dist/assets/index-BqZnnVJF.js +73 -0
  28. package/packages/gui/dist/index.html +1 -1
  29. package/packages/gui/package.json +1 -1
  30. package/packages/gui/src/components/AgentNode.jsx +6 -4
  31. package/packages/gui/src/components/AgentStats.jsx +1 -0
  32. package/packages/gui/src/components/SpawnPanel.jsx +70 -0
  33. package/groove-logo.png +0 -0
  34. package/node_modules/@groove-dev/gui/dist/assets/index-BPVh7Oqk.js +0 -73
  35. 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
- GROOVE DAEMON (:31415)
95
-
96
- │ Registry · Introducer · Lock Manager
97
- │ Journalist · Rotator · Adaptive
98
- │ Classifier · Router · PM · Teams
99
-
100
- │ REST API · WebSocket · GUI Server
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
- Claude Code · Codex · Gemini · Ollama
105
- └─────────────────────────────────────────────┘
111
+ ┌──────────────────────▼───────────────────────┐
112
+ Claude Code · Codex · Gemini CLI · Ollama
113
+ └──────────────────────────────────────────────┘
106
114
  ```
107
115
 
108
116
  ## Links
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.10.9",
3
+ "version": "0.11.0",
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.10.9",
3
+ "version": "0.11.0",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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
- lines.push(`- **${other.name}** (${other.role})scope: ${scope} — ${other.status}`);
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
- lines.push(`| ${escapeMd(a.id)} | ${escapeMd(a.name)} | ${escapeMd(a.role)} | ${escapeMd(a.provider)} | ${scope} | ${escapeMd(a.status)} |`);
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('');