groove-dev 0.8.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 (84) hide show
  1. package/CLAUDE.md +197 -0
  2. package/LICENSE +40 -0
  3. package/README.md +115 -0
  4. package/docs/GUI_DESIGN_SPEC.md +402 -0
  5. package/favicon.png +0 -0
  6. package/groove-logo-short.png +0 -0
  7. package/groove-logo.png +0 -0
  8. package/package.json +70 -0
  9. package/packages/cli/bin/groove.js +98 -0
  10. package/packages/cli/package.json +15 -0
  11. package/packages/cli/src/client.js +25 -0
  12. package/packages/cli/src/commands/agents.js +38 -0
  13. package/packages/cli/src/commands/approve.js +50 -0
  14. package/packages/cli/src/commands/config.js +35 -0
  15. package/packages/cli/src/commands/kill.js +15 -0
  16. package/packages/cli/src/commands/nuke.js +19 -0
  17. package/packages/cli/src/commands/providers.js +40 -0
  18. package/packages/cli/src/commands/rotate.js +16 -0
  19. package/packages/cli/src/commands/spawn.js +91 -0
  20. package/packages/cli/src/commands/start.js +31 -0
  21. package/packages/cli/src/commands/status.js +38 -0
  22. package/packages/cli/src/commands/stop.js +15 -0
  23. package/packages/cli/src/commands/team.js +77 -0
  24. package/packages/daemon/package.json +18 -0
  25. package/packages/daemon/src/adaptive.js +237 -0
  26. package/packages/daemon/src/api.js +533 -0
  27. package/packages/daemon/src/classifier.js +126 -0
  28. package/packages/daemon/src/credentials.js +121 -0
  29. package/packages/daemon/src/firstrun.js +93 -0
  30. package/packages/daemon/src/index.js +208 -0
  31. package/packages/daemon/src/introducer.js +238 -0
  32. package/packages/daemon/src/journalist.js +600 -0
  33. package/packages/daemon/src/lockmanager.js +58 -0
  34. package/packages/daemon/src/pm.js +108 -0
  35. package/packages/daemon/src/process.js +361 -0
  36. package/packages/daemon/src/providers/aider.js +72 -0
  37. package/packages/daemon/src/providers/base.js +38 -0
  38. package/packages/daemon/src/providers/claude-code.js +167 -0
  39. package/packages/daemon/src/providers/codex.js +68 -0
  40. package/packages/daemon/src/providers/gemini.js +62 -0
  41. package/packages/daemon/src/providers/index.js +38 -0
  42. package/packages/daemon/src/providers/ollama.js +94 -0
  43. package/packages/daemon/src/registry.js +89 -0
  44. package/packages/daemon/src/rotator.js +185 -0
  45. package/packages/daemon/src/router.js +132 -0
  46. package/packages/daemon/src/state.js +34 -0
  47. package/packages/daemon/src/supervisor.js +178 -0
  48. package/packages/daemon/src/teams.js +203 -0
  49. package/packages/daemon/src/terminal/base.js +27 -0
  50. package/packages/daemon/src/terminal/generic.js +27 -0
  51. package/packages/daemon/src/terminal/tmux.js +64 -0
  52. package/packages/daemon/src/tokentracker.js +124 -0
  53. package/packages/daemon/src/validate.js +122 -0
  54. package/packages/daemon/templates/api-builder.json +18 -0
  55. package/packages/daemon/templates/fullstack.json +18 -0
  56. package/packages/daemon/templates/monorepo.json +24 -0
  57. package/packages/gui/dist/assets/index-BO95Rm1F.js +73 -0
  58. package/packages/gui/dist/assets/index-CPzm9ZE9.css +1 -0
  59. package/packages/gui/dist/favicon.png +0 -0
  60. package/packages/gui/dist/groove-logo-short.png +0 -0
  61. package/packages/gui/dist/groove-logo.png +0 -0
  62. package/packages/gui/dist/index.html +13 -0
  63. package/packages/gui/index.html +12 -0
  64. package/packages/gui/package.json +22 -0
  65. package/packages/gui/public/favicon.png +0 -0
  66. package/packages/gui/public/groove-logo-short.png +0 -0
  67. package/packages/gui/public/groove-logo.png +0 -0
  68. package/packages/gui/src/App.jsx +215 -0
  69. package/packages/gui/src/components/AgentActions.jsx +347 -0
  70. package/packages/gui/src/components/AgentChat.jsx +479 -0
  71. package/packages/gui/src/components/AgentNode.jsx +117 -0
  72. package/packages/gui/src/components/AgentPanel.jsx +115 -0
  73. package/packages/gui/src/components/AgentStats.jsx +333 -0
  74. package/packages/gui/src/components/ApprovalQueue.jsx +156 -0
  75. package/packages/gui/src/components/EmptyState.jsx +100 -0
  76. package/packages/gui/src/components/SpawnPanel.jsx +515 -0
  77. package/packages/gui/src/components/TeamSelector.jsx +162 -0
  78. package/packages/gui/src/main.jsx +9 -0
  79. package/packages/gui/src/stores/groove.js +247 -0
  80. package/packages/gui/src/theme.css +67 -0
  81. package/packages/gui/src/views/AgentTree.jsx +148 -0
  82. package/packages/gui/src/views/CommandCenter.jsx +620 -0
  83. package/packages/gui/src/views/JournalistFeed.jsx +149 -0
  84. package/packages/gui/vite.config.js +19 -0
@@ -0,0 +1,121 @@
1
+ // GROOVE — Credential Storage (AES-256-GCM encrypted)
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import { readFileSync, writeFileSync, existsSync, chmodSync } from 'fs';
5
+ import { resolve } from 'path';
6
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
7
+ import { hostname, homedir } from 'os';
8
+
9
+ const ALGORITHM = 'aes-256-gcm';
10
+ const SALT_PREFIX = 'groove-v1';
11
+
12
+ export class CredentialStore {
13
+ constructor(grooveDir) {
14
+ this.path = resolve(grooveDir, 'credentials.json');
15
+ this.data = {};
16
+ this.encryptionKey = this.deriveKey();
17
+ this.load();
18
+ }
19
+
20
+ // Derive encryption key from machine-specific data
21
+ // Not unbreakable, but much better than base64 — credentials file is
22
+ // meaningless if copied to another machine or read without this process.
23
+ deriveKey() {
24
+ const machineId = `${SALT_PREFIX}:${homedir()}:${hostname()}`;
25
+ return scryptSync(machineId, 'groove-credential-salt', 32);
26
+ }
27
+
28
+ load() {
29
+ if (existsSync(this.path)) {
30
+ try {
31
+ this.data = JSON.parse(readFileSync(this.path, 'utf8'));
32
+ } catch {
33
+ this.data = {};
34
+ }
35
+ }
36
+ }
37
+
38
+ save() {
39
+ writeFileSync(this.path, JSON.stringify(this.data, null, 2));
40
+ try { chmodSync(this.path, 0o600); } catch { /* Windows */ }
41
+ }
42
+
43
+ setKey(provider, key) {
44
+ if (!provider || typeof provider !== 'string') throw new Error('Provider required');
45
+ if (!key || typeof key !== 'string') throw new Error('Key required');
46
+
47
+ this.data[provider] = {
48
+ key: this.encrypt(key),
49
+ setAt: new Date().toISOString(),
50
+ };
51
+ this.save();
52
+ }
53
+
54
+ getKey(provider) {
55
+ const entry = this.data[provider];
56
+ if (!entry) return null;
57
+ try {
58
+ return this.decrypt(entry.key);
59
+ } catch {
60
+ // Key was encrypted with different machine key, or corrupted
61
+ return null;
62
+ }
63
+ }
64
+
65
+ deleteKey(provider) {
66
+ delete this.data[provider];
67
+ this.save();
68
+ }
69
+
70
+ listProviders() {
71
+ return Object.entries(this.data).map(([provider, entry]) => {
72
+ const key = this.getKey(provider);
73
+ return {
74
+ provider,
75
+ setAt: entry.setAt,
76
+ masked: key ? this.mask(key) : '(unable to decrypt)',
77
+ };
78
+ });
79
+ }
80
+
81
+ hasKey(provider) {
82
+ return !!this.data[provider];
83
+ }
84
+
85
+ getEnvForProvider(provider, providerInfo) {
86
+ const key = this.getKey(provider);
87
+ if (!key || !providerInfo?.envKey) return {};
88
+ return { [providerInfo.envKey]: key };
89
+ }
90
+
91
+ encrypt(text) {
92
+ const iv = randomBytes(16);
93
+ const cipher = createCipheriv(ALGORITHM, this.encryptionKey, iv);
94
+ let encrypted = cipher.update(text, 'utf8', 'hex');
95
+ encrypted += cipher.final('hex');
96
+ const authTag = cipher.getAuthTag();
97
+ // Format: iv:authTag:ciphertext
98
+ return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
99
+ }
100
+
101
+ decrypt(encoded) {
102
+ const parts = encoded.split(':');
103
+ if (parts.length !== 3) {
104
+ // Legacy base64 format — migrate on read
105
+ return Buffer.from(encoded, 'base64').toString('utf8');
106
+ }
107
+ const [ivHex, authTagHex, ciphertext] = parts;
108
+ const iv = Buffer.from(ivHex, 'hex');
109
+ const authTag = Buffer.from(authTagHex, 'hex');
110
+ const decipher = createDecipheriv(ALGORITHM, this.encryptionKey, iv);
111
+ decipher.setAuthTag(authTag);
112
+ let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
113
+ decrypted += decipher.final('utf8');
114
+ return decrypted;
115
+ }
116
+
117
+ mask(key) {
118
+ if (!key || key.length < 8) return '****';
119
+ return key.slice(0, 4) + '...' + key.slice(-4);
120
+ }
121
+ }
@@ -0,0 +1,93 @@
1
+ // GROOVE — First-Run Detection & Setup
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import { existsSync, writeFileSync, readFileSync } from 'fs';
5
+ import { resolve } from 'path';
6
+ import { listProviders } from './providers/index.js';
7
+
8
+ const DEFAULT_CONFIG = {
9
+ version: '0.1.0',
10
+ port: 31415,
11
+ journalistInterval: 120,
12
+ rotationThreshold: 0.75,
13
+ autoRotation: true,
14
+ qcThreshold: 4,
15
+ maxAgents: 10,
16
+ defaultProvider: 'claude-code',
17
+ };
18
+
19
+ export function isFirstRun(grooveDir) {
20
+ return !existsSync(resolve(grooveDir, 'config.json'));
21
+ }
22
+
23
+ // Show welcome banner on every startup
24
+ export function printWelcome(port) {
25
+ const providers = listProviders();
26
+ const installed = providers.filter((p) => p.installed);
27
+ const notInstalled = providers.filter((p) => !p.installed);
28
+
29
+ console.log('');
30
+ console.log(' ┌─────────────────────────────────────┐');
31
+ console.log(' │ Welcome to GROOVE │');
32
+ console.log(' │ Agent orchestration for AI coding │');
33
+ console.log(' └─────────────────────────────────────┘');
34
+ console.log('');
35
+
36
+ if (installed.length > 0) {
37
+ console.log(` Providers (${installed.length} ready):`);
38
+ for (const p of installed) {
39
+ console.log(` ✓ ${p.name}`);
40
+ }
41
+ } else {
42
+ console.log(' No AI providers detected.');
43
+ console.log(' Install at least one: npm i -g @anthropic-ai/claude-code');
44
+ }
45
+
46
+ if (notInstalled.length > 0) {
47
+ console.log('');
48
+ console.log(' Available to install:');
49
+ for (const p of notInstalled) {
50
+ console.log(` · ${p.name.padEnd(18)} ${p.installCommand}`);
51
+ }
52
+ }
53
+
54
+ console.log('');
55
+ console.log(` GUI: http://localhost:${port}`);
56
+ console.log(' Docs: https://docs.groovedev.ai');
57
+ console.log(' GitHub: https://github.com/grooveai-dev/groove');
58
+ console.log('');
59
+ }
60
+
61
+ export function runFirstTimeSetup(grooveDir) {
62
+ // Write default config
63
+ const config = { ...DEFAULT_CONFIG };
64
+
65
+ // Auto-detect best default provider
66
+ const providers = listProviders();
67
+ const installed = providers.filter((p) => p.installed);
68
+ if (installed.length > 0) {
69
+ const preferred = ['claude-code', 'codex', 'gemini', 'aider', 'ollama'];
70
+ const best = preferred.find((id) => installed.some((p) => p.id === id));
71
+ if (best) config.defaultProvider = best;
72
+ }
73
+
74
+ writeFileSync(resolve(grooveDir, 'config.json'), JSON.stringify(config, null, 2));
75
+
76
+ return config;
77
+ }
78
+
79
+ export function loadConfig(grooveDir) {
80
+ const configPath = resolve(grooveDir, 'config.json');
81
+ if (!existsSync(configPath)) return { ...DEFAULT_CONFIG };
82
+
83
+ try {
84
+ const saved = JSON.parse(readFileSync(configPath, 'utf8'));
85
+ return { ...DEFAULT_CONFIG, ...saved };
86
+ } catch {
87
+ return { ...DEFAULT_CONFIG };
88
+ }
89
+ }
90
+
91
+ export function saveConfig(grooveDir, config) {
92
+ writeFileSync(resolve(grooveDir, 'config.json'), JSON.stringify(config, null, 2));
93
+ }
@@ -0,0 +1,208 @@
1
+ // GROOVE Daemon — Entry Point
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import { createServer } from 'http';
5
+ import { resolve } from 'path';
6
+ import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
7
+ import express from 'express';
8
+ import { WebSocketServer } from 'ws';
9
+ import { Registry } from './registry.js';
10
+ import { createApi } from './api.js';
11
+ import { ProcessManager } from './process.js';
12
+ import { StateManager } from './state.js';
13
+ import { Introducer } from './introducer.js';
14
+ import { LockManager } from './lockmanager.js';
15
+ import { Supervisor } from './supervisor.js';
16
+ import { Journalist } from './journalist.js';
17
+ import { TokenTracker } from './tokentracker.js';
18
+ import { Rotator } from './rotator.js';
19
+ import { AdaptiveThresholds } from './adaptive.js';
20
+ import { Teams } from './teams.js';
21
+ import { CredentialStore } from './credentials.js';
22
+ import { TaskClassifier } from './classifier.js';
23
+ import { ModelRouter } from './router.js';
24
+ import { ProjectManager } from './pm.js';
25
+ import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
26
+
27
+ const DEFAULT_PORT = 31415;
28
+
29
+ export { loadConfig, saveConfig } from './firstrun.js';
30
+
31
+ export class Daemon {
32
+ constructor(options = {}) {
33
+ this.port = options.port !== undefined ? options.port : (parseInt(process.env.GROOVE_PORT, 10) || DEFAULT_PORT);
34
+ this.projectDir = options.projectDir || process.cwd();
35
+ this.grooveDir = options.grooveDir || resolve(this.projectDir, '.groove');
36
+ this.pidFile = resolve(this.grooveDir, 'daemon.pid');
37
+
38
+ // Ensure .groove directories exist
39
+ mkdirSync(resolve(this.grooveDir, 'logs'), { recursive: true });
40
+ mkdirSync(resolve(this.grooveDir, 'context'), { recursive: true });
41
+
42
+ // Initialize coordination file for agent knock protocol
43
+ const coordPath = resolve(this.grooveDir, 'coordination.md');
44
+ if (!existsSync(coordPath)) {
45
+ writeFileSync(coordPath, '# GROOVE Coordination\n\n*Agents write their intent here before shared/destructive actions.*\n\n<!-- No active operations -->\n');
46
+ }
47
+
48
+ // First-run detection
49
+ if (isFirstRun(this.grooveDir)) {
50
+ this.config = runFirstTimeSetup(this.grooveDir);
51
+ } else {
52
+ this.config = loadConfig(this.grooveDir);
53
+ }
54
+
55
+ // Initialize core components
56
+ this.state = new StateManager(this.grooveDir);
57
+ this.registry = new Registry(this.state);
58
+ this.locks = new LockManager(this.grooveDir);
59
+ this.tokens = new TokenTracker(this.grooveDir);
60
+ this.processes = new ProcessManager(this);
61
+ this.introducer = new Introducer(this);
62
+ this.supervisor = new Supervisor(this);
63
+ this.journalist = new Journalist(this);
64
+ this.rotator = new Rotator(this);
65
+ this.adaptive = new AdaptiveThresholds(this.grooveDir);
66
+ this.teams = new Teams(this);
67
+ this.credentials = new CredentialStore(this.grooveDir);
68
+ this.classifier = new TaskClassifier();
69
+ this.router = new ModelRouter(this);
70
+ this.pm = new ProjectManager(this);
71
+
72
+ // HTTP + WebSocket server
73
+ this.app = express();
74
+ this.server = createServer(this.app);
75
+ this.wss = new WebSocketServer({
76
+ server: this.server,
77
+ maxPayload: 1024 * 1024, // 1MB max message
78
+ verifyClient: ({ req }) => {
79
+ const origin = req.headers.origin;
80
+ // Allow: no origin (CLI/native clients), localhost origins
81
+ if (!origin) return true;
82
+ const allowed = [
83
+ `http://localhost:${this.port}`,
84
+ `http://127.0.0.1:${this.port}`,
85
+ 'http://localhost:3142',
86
+ ];
87
+ return allowed.includes(origin);
88
+ },
89
+ });
90
+
91
+ // Wire up API routes
92
+ createApi(this.app, this);
93
+
94
+ // Broadcast registry changes over WebSocket
95
+ this.registry.on('change', () => {
96
+ this.broadcast({ type: 'state', data: this.registry.getAll() });
97
+ });
98
+
99
+ // Send full state to new WebSocket clients
100
+ this.wss.on('connection', (ws) => {
101
+ ws.send(JSON.stringify({
102
+ type: 'state',
103
+ data: this.registry.getAll(),
104
+ }));
105
+ });
106
+
107
+ // Auto-update AGENTS_REGISTRY.md and CLAUDE.md GROOVE section on changes
108
+ this.registry.on('change', () => {
109
+ this.introducer.writeRegistryFile(this.projectDir);
110
+ this.introducer.injectGrooveSection(this.projectDir);
111
+ this.teams.onAgentChange();
112
+ this.supervisor.checkQcThreshold();
113
+ });
114
+ }
115
+
116
+ broadcast(message) {
117
+ const payload = JSON.stringify(message);
118
+ for (const client of this.wss.clients) {
119
+ if (client.readyState === 1) {
120
+ client.send(payload);
121
+ }
122
+ }
123
+ }
124
+
125
+ async start() {
126
+ // Check for existing daemon
127
+ if (existsSync(this.pidFile)) {
128
+ const existingPid = parseInt(readFileSync(this.pidFile, 'utf8'), 10);
129
+ try {
130
+ process.kill(existingPid, 0); // Signal 0 = check if alive
131
+ console.error(`GROOVE daemon already running (PID ${existingPid})`);
132
+ process.exit(1);
133
+ } catch {
134
+ // PID file is stale — previous daemon crashed
135
+ unlinkSync(this.pidFile);
136
+ }
137
+ }
138
+
139
+ // Restore persisted state
140
+ this.state.load();
141
+ this.registry.restore(this.state.get('agents') || []);
142
+
143
+ return new Promise((resolvePromise) => {
144
+ this.server.listen(this.port, '127.0.0.1', () => {
145
+ writeFileSync(this.pidFile, String(process.pid));
146
+
147
+ printWelcome(this.port);
148
+
149
+ // Start background services
150
+ this.journalist.start();
151
+ this.rotator.start();
152
+
153
+ resolvePromise(this);
154
+ });
155
+ });
156
+ }
157
+
158
+ async stop() {
159
+ // Persist state before shutdown
160
+ this.state.set('agents', this.registry.getAll());
161
+ this.state.save();
162
+
163
+ // Stop background services
164
+ this.journalist.stop();
165
+ this.rotator.stop();
166
+
167
+ // Kill all agent processes
168
+ await this.processes.killAll();
169
+
170
+ // Clean up PID file
171
+ if (existsSync(this.pidFile)) {
172
+ unlinkSync(this.pidFile);
173
+ }
174
+
175
+ // Clean up generated files
176
+ const registryPath = resolve(this.projectDir, 'AGENTS_REGISTRY.md');
177
+ if (existsSync(registryPath)) {
178
+ unlinkSync(registryPath);
179
+ }
180
+ this.introducer.removeGrooveSection(this.projectDir);
181
+
182
+ // Close server
183
+ return new Promise((resolvePromise) => {
184
+ this.wss.close(() => {
185
+ this.server.close(() => {
186
+ console.log('GROOVE daemon stopped.');
187
+ resolvePromise();
188
+ });
189
+ });
190
+ });
191
+ }
192
+ }
193
+
194
+ // Start daemon if run directly
195
+ const isDirectRun = process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
196
+ if (isDirectRun) {
197
+ const daemon = new Daemon();
198
+
199
+ const shutdown = async () => {
200
+ await daemon.stop();
201
+ process.exit(0);
202
+ };
203
+
204
+ process.on('SIGINT', shutdown);
205
+ process.on('SIGTERM', shutdown);
206
+
207
+ daemon.start();
208
+ }
@@ -0,0 +1,238 @@
1
+ // GROOVE — Introduction Protocol
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import { writeFileSync, readFileSync, existsSync } from 'fs';
5
+ import { resolve } from 'path';
6
+ import { escapeMd } from './validate.js';
7
+
8
+ const GROOVE_SECTION_START = '<!-- GROOVE:START -->';
9
+ const GROOVE_SECTION_END = '<!-- GROOVE:END -->';
10
+
11
+ export class Introducer {
12
+ constructor(daemon) {
13
+ this.daemon = daemon;
14
+ }
15
+
16
+ generateContext(newAgent, options = {}) {
17
+ const { taskNegotiation } = options;
18
+ const agents = this.daemon.registry.getAll();
19
+ // Include ALL agents (running + completed) so new agents know what the team did
20
+ const others = agents.filter((a) => a.id !== newAgent.id &&
21
+ (a.status === 'running' || a.status === 'starting' || a.status === 'completed'));
22
+
23
+ const lines = [
24
+ `# GROOVE Agent Context`,
25
+ ``,
26
+ `You are **${newAgent.name}** (role: ${newAgent.role}), managed by GROOVE.`,
27
+ ];
28
+
29
+ if (newAgent.scope && newAgent.scope.length > 0) {
30
+ lines.push(`Your file scope: \`${newAgent.scope.join('`, `')}\``);
31
+ } else {
32
+ lines.push(`You have no file scope restrictions.`);
33
+ }
34
+
35
+ lines.push('');
36
+
37
+ if (others.length === 0) {
38
+ lines.push('You are the only agent on this project right now.');
39
+ } else {
40
+ lines.push(`## Team (${others.length} other agent${others.length > 1 ? 's' : ''})`);
41
+ lines.push('');
42
+
43
+ // Collect all files created by teammates for the project files section
44
+ const allTeamFiles = [];
45
+
46
+ for (const other of others) {
47
+ const scope = other.scope?.length > 0 ? other.scope.join(', ') : 'unrestricted';
48
+ lines.push(`- **${other.name}** (${other.role}) — scope: ${scope} — ${other.status}`);
49
+
50
+ // Get files this agent created/modified
51
+ const files = this.daemon.journalist?.getAgentFiles(other) || [];
52
+ if (files.length > 0) {
53
+ const shown = files.slice(0, 15);
54
+ lines.push(` Files: ${shown.join(', ')}${files.length > 15 ? ` (+${files.length - 15} more)` : ''}`);
55
+ for (const f of files) {
56
+ allTeamFiles.push({ file: f, agent: other.name, role: other.role });
57
+ }
58
+ }
59
+
60
+ // For completed agents, include their final result summary
61
+ if (other.status === 'completed') {
62
+ const result = this.daemon.journalist?.getAgentResult(other) || '';
63
+ if (result) {
64
+ lines.push(` Result: ${result.slice(0, 500)}`);
65
+ }
66
+ }
67
+ }
68
+
69
+ lines.push('');
70
+ lines.push(`## Coordination Rules`);
71
+ lines.push('');
72
+ lines.push(`- Stay within your file scope. Do NOT modify files owned by other agents.`);
73
+ lines.push(`- If you need changes outside your scope, document what you need — GROOVE will coordinate.`);
74
+ lines.push(`- Check AGENTS_REGISTRY.md for the latest team state.`);
75
+
76
+ // Project files section — tell the new agent what exists and what to read
77
+ if (allTeamFiles.length > 0) {
78
+ lines.push('');
79
+ lines.push(`## Project Files`);
80
+ lines.push('');
81
+ lines.push(`Your team has created the following files. **Read relevant ones before starting work** to understand what's been built and planned:`);
82
+ lines.push('');
83
+
84
+ // Group by agent for clarity
85
+ const byAgent = {};
86
+ for (const { file, agent, role } of allTeamFiles) {
87
+ const key = `${agent} (${role})`;
88
+ if (!byAgent[key]) byAgent[key] = [];
89
+ byAgent[key].push(file);
90
+ }
91
+ for (const [agent, files] of Object.entries(byAgent)) {
92
+ lines.push(`**${agent}:**`);
93
+ for (const f of files.slice(0, 20)) {
94
+ lines.push(`- ${f}`);
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ // Task negotiation — when a duplicate role joins, include the work division
101
+ if (taskNegotiation) {
102
+ lines.push('');
103
+ lines.push(`## Task Assignment`);
104
+ lines.push('');
105
+ lines.push(`A task coordinator has analyzed the current team's work and assigned your focus area:`);
106
+ lines.push('');
107
+ lines.push(taskNegotiation);
108
+ lines.push('');
109
+ lines.push(`**Follow this assignment.** Focus on your assigned tasks and do NOT modify files that other same-role agents are actively working on.`);
110
+ }
111
+
112
+ // Knock protocol — coordination for destructive/shared actions
113
+ const running = others.filter((a) => a.status === 'running' || a.status === 'starting');
114
+ if (running.length > 0) {
115
+ lines.push('');
116
+ lines.push(`## Coordination Protocol`);
117
+ lines.push('');
118
+ lines.push(`Before performing shared/destructive actions (restart server, npm install/build, modify package.json, modify shared config), coordinate with your team:`);
119
+ lines.push(`1. Read \`.groove/coordination.md\` to check for active operations`);
120
+ lines.push(`2. Write your intent to \`.groove/coordination.md\` (e.g., "backend-1: restarting server")`);
121
+ lines.push(`3. Proceed only if no conflicting operations are active`);
122
+ lines.push(`4. Clear your entry from \`.groove/coordination.md\` when done`);
123
+ }
124
+
125
+ // Memory containment — prevent agents from reading/writing auto-memory
126
+ // which can contain stale context from unrelated sessions in the same dir
127
+ lines.push('');
128
+ lines.push(`## Memory Policy`);
129
+ lines.push('');
130
+ lines.push(`Ignore auto-memory. Do NOT read or write MEMORY.md or any files in the auto-memory directory.`);
131
+ lines.push(`GROOVE provides all your project context through handoff briefs, AGENTS_REGISTRY.md, and GROOVE_PROJECT_MAP.md.`);
132
+ lines.push(`Do NOT save memories — your state is managed by GROOVE's rotation and handoff system.`);
133
+
134
+ // Add reference to project map if it exists
135
+ const mapPath = resolve(this.daemon.projectDir, 'GROOVE_PROJECT_MAP.md');
136
+ if (existsSync(mapPath)) {
137
+ lines.push('');
138
+ lines.push(`Read GROOVE_PROJECT_MAP.md for current project context from The Journalist.`);
139
+ }
140
+
141
+ return lines.join('\n');
142
+ }
143
+
144
+ writeRegistryFile(projectDir) {
145
+ const agents = this.daemon.registry.getAll();
146
+
147
+ if (agents.length === 0) {
148
+ // Clean up if no agents
149
+ const regPath = resolve(projectDir, 'AGENTS_REGISTRY.md');
150
+ if (existsSync(regPath)) {
151
+ writeFileSync(regPath, '');
152
+ }
153
+ return;
154
+ }
155
+
156
+ const lines = [
157
+ `# AGENTS REGISTRY`,
158
+ ``,
159
+ `*Auto-generated by GROOVE. Do not edit manually.*`,
160
+ ``,
161
+ `| ID | Name | Role | Provider | Scope | Status |`,
162
+ `|----|------|------|----------|-------|--------|`,
163
+ ];
164
+
165
+ for (const a of agents) {
166
+ 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)} |`);
168
+ }
169
+
170
+ lines.push('');
171
+ lines.push(`*Updated: ${new Date().toISOString()}*`);
172
+
173
+ writeFileSync(resolve(projectDir, 'AGENTS_REGISTRY.md'), lines.join('\n'));
174
+ }
175
+
176
+ injectGrooveSection(projectDir) {
177
+ // Inject a GROOVE section into the project's CLAUDE.md.
178
+ // This section is delimited by markers so we can update it without
179
+ // clobbering the user's content.
180
+ const claudeMdPath = resolve(projectDir, 'CLAUDE.md');
181
+ const agents = this.daemon.registry.getAll();
182
+ const running = agents.filter((a) => a.status === 'running');
183
+
184
+ const grooveContent = [
185
+ GROOVE_SECTION_START,
186
+ '',
187
+ '## GROOVE Orchestration (auto-injected)',
188
+ '',
189
+ `Active agents: ${running.length}`,
190
+ '',
191
+ running.length > 0 ? '| Name | Role | Scope |' : '',
192
+ running.length > 0 ? '|------|------|-------|' : '',
193
+ ...running.map((a) => `| ${a.name} | ${a.role} | ${a.scope?.join(', ') || '-'} |`),
194
+ '',
195
+ `See AGENTS_REGISTRY.md for full agent state.`,
196
+ '',
197
+ `**Memory policy:** Ignore auto-memory. Do not read or write MEMORY.md. GROOVE manages all context.`,
198
+ '',
199
+ GROOVE_SECTION_END,
200
+ ].filter(Boolean).join('\n');
201
+
202
+ if (!existsSync(claudeMdPath)) {
203
+ return; // Don't create CLAUDE.md — it's the user's file
204
+ }
205
+
206
+ let content = readFileSync(claudeMdPath, 'utf8');
207
+
208
+ const startIdx = content.indexOf(GROOVE_SECTION_START);
209
+ const endIdx = content.indexOf(GROOVE_SECTION_END);
210
+
211
+ if (startIdx !== -1 && endIdx !== -1) {
212
+ // Replace existing GROOVE section
213
+ content = content.slice(0, startIdx) + grooveContent + content.slice(endIdx + GROOVE_SECTION_END.length);
214
+ } else {
215
+ // Append GROOVE section
216
+ content = content.trimEnd() + '\n\n' + grooveContent + '\n';
217
+ }
218
+
219
+ writeFileSync(claudeMdPath, content);
220
+ }
221
+
222
+ removeGrooveSection(projectDir) {
223
+ const claudeMdPath = resolve(projectDir, 'CLAUDE.md');
224
+ if (!existsSync(claudeMdPath)) return;
225
+
226
+ let content = readFileSync(claudeMdPath, 'utf8');
227
+ const startIdx = content.indexOf(GROOVE_SECTION_START);
228
+ const endIdx = content.indexOf(GROOVE_SECTION_END);
229
+
230
+ if (startIdx !== -1 && endIdx !== -1) {
231
+ // Remove the GROOVE section and any surrounding blank lines
232
+ const before = content.slice(0, startIdx).replace(/\n+$/, '');
233
+ const after = content.slice(endIdx + GROOVE_SECTION_END.length).replace(/^\n+/, '');
234
+ content = before + (after ? '\n\n' + after : '') + '\n';
235
+ writeFileSync(claudeMdPath, content);
236
+ }
237
+ }
238
+ }