groove-dev 0.27.7 → 0.27.11

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 (127) hide show
  1. package/CLAUDE.md +0 -7
  2. package/node_modules/@groove-dev/daemon/src/api.js +496 -44
  3. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +25 -12
  4. package/node_modules/@groove-dev/daemon/src/index.js +7 -0
  5. package/node_modules/@groove-dev/daemon/src/introducer.js +72 -4
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +66 -11
  7. package/node_modules/@groove-dev/daemon/src/process.js +128 -104
  8. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  9. package/node_modules/@groove-dev/daemon/src/repo-import.js +541 -0
  10. package/node_modules/@groove-dev/daemon/src/rotator.js +28 -1
  11. package/node_modules/@groove-dev/daemon/src/supervisor.js +2 -1
  12. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +504 -0
  13. package/node_modules/@groove-dev/daemon/src/validate.js +13 -0
  14. package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -4
  15. package/node_modules/@groove-dev/daemon/test/rotator.test.js +4 -1
  16. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +1 -0
  17. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +677 -0
  18. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  19. package/node_modules/@groove-dev/gui/src/app.css +14 -0
  20. package/node_modules/@groove-dev/gui/src/app.jsx +13 -0
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +130 -1
  22. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  23. package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  24. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +16 -17
  25. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +141 -1
  26. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  27. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +8 -8
  28. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  29. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  30. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +4 -4
  31. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +7 -1
  32. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  33. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +14 -4
  34. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +46 -11
  35. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-card.jsx +64 -0
  36. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +363 -0
  37. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  38. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +22 -0
  39. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +48 -0
  40. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +129 -0
  41. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +243 -0
  42. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +192 -0
  43. package/node_modules/@groove-dev/gui/src/components/ui/approval-modal.jsx +63 -0
  44. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
  45. package/node_modules/@groove-dev/gui/src/lib/edition.js +4 -0
  46. package/node_modules/@groove-dev/gui/src/lib/electron.js +17 -0
  47. package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
  48. package/node_modules/@groove-dev/gui/src/stores/groove.js +150 -6
  49. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +39 -40
  50. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +82 -0
  51. package/node_modules/@groove-dev/gui/src/views/settings.jsx +66 -0
  52. package/node_modules/@groove-dev/gui/vite.config.js +3 -0
  53. package/package.json +7 -2
  54. package/packages/daemon/src/api.js +496 -44
  55. package/packages/daemon/src/gateways/manager.js +25 -12
  56. package/packages/daemon/src/index.js +7 -0
  57. package/packages/daemon/src/introducer.js +72 -4
  58. package/packages/daemon/src/journalist.js +66 -11
  59. package/packages/daemon/src/process.js +128 -104
  60. package/packages/daemon/src/registry.js +1 -1
  61. package/packages/daemon/src/repo-import.js +541 -0
  62. package/packages/daemon/src/rotator.js +28 -1
  63. package/packages/daemon/src/supervisor.js +2 -1
  64. package/packages/daemon/src/tunnel-manager.js +504 -0
  65. package/packages/daemon/src/validate.js +13 -0
  66. package/packages/gui/dist/assets/index-BE6lYcd7.css +1 -0
  67. package/packages/gui/dist/assets/index-zdzOLAZM.js +677 -0
  68. package/packages/gui/dist/index.html +2 -2
  69. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +3 -3
  70. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +2 -2
  71. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +3 -3
  72. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +5 -5
  73. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-dialog.js +3 -3
  74. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-scroll-area.js +1 -1
  75. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tabs.js +5 -5
  76. package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tooltip.js +3 -3
  77. package/packages/gui/node_modules/.vite/deps/_metadata.json +53 -53
  78. package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js → chunk-DH7AESXW.js} +2 -2
  79. package/packages/gui/node_modules/.vite/deps/{chunk-KXLIKZFX.js → chunk-GFE3G4IN.js} +133 -133
  80. package/packages/gui/node_modules/.vite/deps/chunk-GFE3G4IN.js.map +7 -0
  81. package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js → chunk-LKZVMLRH.js} +6 -6
  82. package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js → chunk-MCVDVNE5.js} +2 -2
  83. package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js → chunk-SPKVQGZX.js} +6 -6
  84. package/packages/gui/src/app.css +14 -0
  85. package/packages/gui/src/app.jsx +13 -0
  86. package/packages/gui/src/components/agents/agent-config.jsx +130 -1
  87. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  88. package/packages/gui/src/components/agents/agent-mdfiles.jsx +43 -1
  89. package/packages/gui/src/components/agents/agent-node.jsx +16 -17
  90. package/packages/gui/src/components/agents/spawn-wizard.jsx +141 -1
  91. package/packages/gui/src/components/dashboard/fleet-panel.jsx +3 -3
  92. package/packages/gui/src/components/dashboard/intel-panel.jsx +8 -8
  93. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  94. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  95. package/packages/gui/src/components/layout/activity-bar.jsx +4 -4
  96. package/packages/gui/src/components/layout/app-shell.jsx +7 -1
  97. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
  98. package/packages/gui/src/components/layout/command-palette.jsx +14 -4
  99. package/packages/gui/src/components/layout/status-bar.jsx +46 -11
  100. package/packages/gui/src/components/marketplace/repo-card.jsx +64 -0
  101. package/packages/gui/src/components/marketplace/repo-import.jsx +363 -0
  102. package/packages/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
  103. package/packages/gui/src/components/pro/pro-gate.jsx +22 -0
  104. package/packages/gui/src/components/pro/upgrade-card.jsx +48 -0
  105. package/packages/gui/src/components/settings/quick-connect.jsx +129 -0
  106. package/packages/gui/src/components/settings/remote-server-card.jsx +243 -0
  107. package/packages/gui/src/components/settings/server-dialog.jsx +192 -0
  108. package/packages/gui/src/components/ui/approval-modal.jsx +63 -0
  109. package/packages/gui/src/components/ui/toast.jsx +1 -1
  110. package/packages/gui/src/lib/edition.js +4 -0
  111. package/packages/gui/src/lib/electron.js +17 -0
  112. package/packages/gui/src/lib/status.js +1 -0
  113. package/packages/gui/src/stores/groove.js +150 -6
  114. package/packages/gui/src/views/dashboard.jsx +39 -40
  115. package/packages/gui/src/views/marketplace.jsx +82 -0
  116. package/packages/gui/src/views/settings.jsx +66 -0
  117. package/packages/gui/vite.config.js +3 -0
  118. package/node_modules/@groove-dev/gui/dist/assets/index-Bl1_J0sN.js +0 -652
  119. package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +0 -1
  120. package/packages/gui/dist/assets/index-Bl1_J0sN.js +0 -652
  121. package/packages/gui/dist/assets/index-DjORRpF0.css +0 -1
  122. package/packages/gui/node_modules/.vite/deps/chunk-KXLIKZFX.js.map +0 -7
  123. package/test-slack.mjs +0 -28
  124. /package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js.map → chunk-DH7AESXW.js.map} +0 -0
  125. /package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js.map → chunk-LKZVMLRH.js.map} +0 -0
  126. /package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js.map → chunk-MCVDVNE5.js.map} +0 -0
  127. /package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js.map → chunk-SPKVQGZX.js.map} +0 -0
@@ -0,0 +1,541 @@
1
+ // GROOVE — Repo Import (GitHub Clone + Sandbox Isolation)
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync, readdirSync, copyFileSync } from 'fs';
5
+ import { resolve, basename, dirname, isAbsolute } from 'path';
6
+ import { execFileSync } from 'child_process';
7
+ import { randomUUID } from 'crypto';
8
+ import { homedir } from 'os';
9
+
10
+ const GITHUB_URL_RE = /^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?(?:\/.*)?$/;
11
+
12
+ function parseGitHubUrl(url) {
13
+ const m = url.match(GITHUB_URL_RE);
14
+ if (!m) throw new Error('Invalid GitHub URL. Expected: https://github.com/owner/repo');
15
+ return { owner: m[1], repo: m[2] };
16
+ }
17
+
18
+ export class RepoImporter {
19
+ constructor(daemon) {
20
+ this.daemon = daemon;
21
+ this.imports = new Map();
22
+ this.importsDir = resolve(daemon.grooveDir, 'imports');
23
+ mkdirSync(this.importsDir, { recursive: true });
24
+ this._loadExisting();
25
+ }
26
+
27
+ _loadExisting() {
28
+ try {
29
+ const files = readdirSync(this.importsDir).filter(f => f.endsWith('.json'));
30
+ for (const f of files) {
31
+ try {
32
+ const data = JSON.parse(readFileSync(resolve(this.importsDir, f), 'utf8'));
33
+ if (data.id) this.imports.set(data.id, data);
34
+ } catch { /* skip corrupt manifests */ }
35
+ }
36
+ } catch { /* dir may not exist yet */ }
37
+ }
38
+
39
+ _saveManifest(manifest) {
40
+ writeFileSync(
41
+ resolve(this.importsDir, `${manifest.id}.json`),
42
+ JSON.stringify(manifest, null, 2)
43
+ );
44
+ this.imports.set(manifest.id, manifest);
45
+ }
46
+
47
+ // --- Preview (no clone, no disk writes) ---
48
+
49
+ async preview(repoUrl) {
50
+ const { owner, repo } = parseGitHubUrl(repoUrl);
51
+
52
+ let repoData;
53
+ let treeData;
54
+
55
+ // Try GitHub MCP integration first, fall back to fetch
56
+ const mcp = this.daemon.mcpManager;
57
+ const hasMcp = mcp && typeof mcp.execTool === 'function';
58
+
59
+ if (hasMcp) {
60
+ try {
61
+ repoData = await mcp.execTool('github', 'get_repository', { owner, repo });
62
+ } catch { /* fall through to fetch */ }
63
+ }
64
+
65
+ if (!repoData) {
66
+ const headers = { 'User-Agent': 'groove-dev', Accept: 'application/vnd.github+json' };
67
+ const pat = this._getPat();
68
+ if (pat) headers.Authorization = `Bearer ${pat}`;
69
+
70
+ const resp = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers });
71
+ if (!resp.ok) throw new Error(`GitHub API error: ${resp.status} ${resp.statusText}`);
72
+ repoData = await resp.json();
73
+ }
74
+
75
+ // Fetch top-level tree for file detection
76
+ const detectedFiles = [];
77
+ try {
78
+ const headers = { 'User-Agent': 'groove-dev', Accept: 'application/vnd.github+json' };
79
+ const pat = this._getPat();
80
+ if (pat) headers.Authorization = `Bearer ${pat}`;
81
+
82
+ const branch = repoData.default_branch || 'main';
83
+ const treeResp = await fetch(
84
+ `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=0`,
85
+ { headers }
86
+ );
87
+ if (treeResp.ok) {
88
+ treeData = await treeResp.json();
89
+ if (treeData.tree) {
90
+ for (const entry of treeData.tree) {
91
+ detectedFiles.push(entry.path);
92
+ }
93
+ }
94
+ }
95
+ } catch { /* tree fetch is optional */ }
96
+
97
+ // Fetch README preview
98
+ let readmePreview = '';
99
+ try {
100
+ const headers = { 'User-Agent': 'groove-dev', Accept: 'application/vnd.github.raw' };
101
+ const pat = this._getPat();
102
+ if (pat) headers.Authorization = `Bearer ${pat}`;
103
+
104
+ const readmeResp = await fetch(
105
+ `https://api.github.com/repos/${owner}/${repo}/readme`,
106
+ { headers: { ...headers, Accept: 'application/vnd.github+json' } }
107
+ );
108
+ if (readmeResp.ok) {
109
+ const readmeJson = await readmeResp.json();
110
+ if (readmeJson.content) {
111
+ const decoded = Buffer.from(readmeJson.content, 'base64').toString('utf8');
112
+ readmePreview = decoded.slice(0, 800);
113
+ }
114
+ }
115
+ } catch { /* readme is optional */ }
116
+
117
+ const stackHints = this._stackHintsFromFiles(detectedFiles);
118
+
119
+ return {
120
+ owner,
121
+ repo,
122
+ name: repoData.name || repo,
123
+ description: repoData.description || '',
124
+ language: repoData.language || null,
125
+ stars: repoData.stargazers_count ?? 0,
126
+ license: repoData.license?.spdx_id || repoData.license?.name || null,
127
+ defaultBranch: repoData.default_branch || 'main',
128
+ detectedFiles,
129
+ readmePreview,
130
+ stackHints,
131
+ };
132
+ }
133
+
134
+ _stackHintsFromFiles(files) {
135
+ const has = (f) => files.includes(f);
136
+ const hints = {};
137
+ if (has('bun.lockb')) { hints.runtime = 'bun'; hints.packageManager = 'bun'; }
138
+ else if (has('pnpm-lock.yaml')) { hints.runtime = 'node'; hints.packageManager = 'pnpm'; }
139
+ else if (has('yarn.lock')) { hints.runtime = 'node'; hints.packageManager = 'yarn'; }
140
+ else if (has('package-lock.json')) { hints.runtime = 'node'; hints.packageManager = 'npm'; }
141
+ else if (has('package.json')) { hints.runtime = 'node'; hints.packageManager = 'npm'; }
142
+ if (has('requirements.txt') || has('pyproject.toml')) hints.runtime = hints.runtime || 'python';
143
+ if (has('go.mod')) hints.runtime = hints.runtime || 'go';
144
+ if (has('Cargo.toml')) hints.runtime = hints.runtime || 'rust';
145
+ if (has('Gemfile')) hints.runtime = hints.runtime || 'ruby';
146
+ if (has('composer.json')) hints.runtime = hints.runtime || 'php';
147
+ if (has('tsconfig.json')) hints.language = 'typescript';
148
+ if (has('docker-compose.yml')) hints.hasDocker = true;
149
+ if (has('.env.example')) hints.hasEnvExample = true;
150
+ return hints;
151
+ }
152
+
153
+ // --- Stack Detection (on cloned repo) ---
154
+
155
+ detectStack(repoPath) {
156
+ const has = (f) => existsSync(resolve(repoPath, f));
157
+ const read = (f) => {
158
+ try { return readFileSync(resolve(repoPath, f), 'utf8'); } catch { return null; }
159
+ };
160
+
161
+ let runtime = null;
162
+ let language = null;
163
+ let packageManager = null;
164
+ let installCommand = null;
165
+ let buildCommand = null;
166
+ let testCommand = null;
167
+
168
+ if (has('bun.lockb')) {
169
+ runtime = 'bun'; packageManager = 'bun';
170
+ installCommand = 'bun install'; buildCommand = 'bun run build'; testCommand = 'bun test';
171
+ } else if (has('pnpm-lock.yaml')) {
172
+ runtime = 'node'; packageManager = 'pnpm';
173
+ installCommand = 'pnpm install'; buildCommand = 'pnpm build'; testCommand = 'pnpm test';
174
+ } else if (has('yarn.lock')) {
175
+ runtime = 'node'; packageManager = 'yarn';
176
+ installCommand = 'yarn install'; buildCommand = 'yarn build'; testCommand = 'yarn test';
177
+ } else if (has('package-lock.json') || has('package.json')) {
178
+ runtime = 'node'; packageManager = 'npm';
179
+ installCommand = 'npm install'; buildCommand = 'npm run build'; testCommand = 'npm test';
180
+ } else if (has('requirements.txt')) {
181
+ runtime = 'python'; language = 'python';
182
+ installCommand = 'pip install -r requirements.txt';
183
+ } else if (has('pyproject.toml')) {
184
+ runtime = 'python'; language = 'python';
185
+ const content = read('pyproject.toml') || '';
186
+ if (content.includes('[tool.poetry]')) {
187
+ packageManager = 'poetry'; installCommand = 'poetry install';
188
+ } else {
189
+ packageManager = 'uv'; installCommand = 'uv sync';
190
+ }
191
+ } else if (has('go.mod')) {
192
+ runtime = 'go'; language = 'go';
193
+ installCommand = 'go mod download'; buildCommand = 'go build ./...'; testCommand = 'go test ./...';
194
+ } else if (has('Cargo.toml')) {
195
+ runtime = 'rust'; language = 'rust';
196
+ installCommand = 'cargo build'; buildCommand = 'cargo build --release'; testCommand = 'cargo test';
197
+ } else if (has('Gemfile')) {
198
+ runtime = 'ruby'; language = 'ruby';
199
+ installCommand = 'bundle install';
200
+ } else if (has('composer.json')) {
201
+ runtime = 'php'; language = 'php';
202
+ installCommand = 'composer install';
203
+ }
204
+
205
+ if (has('tsconfig.json')) language = 'typescript';
206
+ if (!language && has('package.json')) {
207
+ const pkg = read('package.json');
208
+ if (pkg) {
209
+ try {
210
+ const parsed = JSON.parse(pkg);
211
+ if (parsed.dependencies?.typescript || parsed.devDependencies?.typescript) language = 'typescript';
212
+ } catch { /* skip */ }
213
+ }
214
+ }
215
+ if (!language && runtime === 'node') language = 'javascript';
216
+
217
+ const hasDocker = has('docker-compose.yml');
218
+ const hasEnvExample = has('.env.example');
219
+
220
+ let envVars = [];
221
+ if (hasEnvExample) {
222
+ envVars = this._parseEnvExample(read('.env.example') || '');
223
+ }
224
+
225
+ return {
226
+ runtime,
227
+ language,
228
+ packageManager,
229
+ hasDocker,
230
+ hasEnvExample,
231
+ envVars,
232
+ installCommand,
233
+ buildCommand,
234
+ testCommand,
235
+ };
236
+ }
237
+
238
+ _parseEnvExample(content) {
239
+ const vars = [];
240
+ for (const line of content.split('\n')) {
241
+ const trimmed = line.trim();
242
+ if (!trimmed || trimmed.startsWith('#')) continue;
243
+ const eqIdx = trimmed.indexOf('=');
244
+ if (eqIdx < 1) continue;
245
+ const key = trimmed.slice(0, eqIdx).trim();
246
+ const rest = trimmed.slice(eqIdx + 1).trim();
247
+ let hint = '';
248
+ const commentIdx = rest.indexOf('#');
249
+ if (commentIdx !== -1) hint = rest.slice(commentIdx + 1).trim();
250
+ vars.push({ key, hint });
251
+ }
252
+ return vars;
253
+ }
254
+
255
+ // --- Import (clone + sandbox) ---
256
+
257
+ async import(repoUrl, targetPath, options = {}) {
258
+ // Resolve ~ and relative paths
259
+ if (targetPath.startsWith('~/') || targetPath === '~') {
260
+ targetPath = resolve(homedir(), targetPath.slice(2));
261
+ } else if (!isAbsolute(targetPath)) {
262
+ targetPath = resolve(this.daemon.projectDir, targetPath);
263
+ }
264
+ if (targetPath.includes('..')) throw new Error('Path traversal not allowed');
265
+ const parentDir = dirname(targetPath);
266
+ if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
267
+ if (existsSync(targetPath)) throw new Error(`Target path already exists: ${targetPath}`);
268
+
269
+ // Check for duplicate import
270
+ for (const manifest of this.imports.values()) {
271
+ if (manifest.repoUrl === repoUrl && manifest.status === 'active') {
272
+ throw new Error(`Already imported from ${repoUrl} at ${manifest.clonedTo}`);
273
+ }
274
+ }
275
+
276
+ const { owner, repo } = parseGitHubUrl(repoUrl);
277
+ const importId = randomUUID().slice(0, 12);
278
+
279
+ // Clone
280
+ const cloneArgs = ['clone', '--depth', '1', repoUrl, targetPath];
281
+ const pat = this._getPat();
282
+ let cloneUrl = repoUrl;
283
+ if (pat && repoUrl.startsWith('https://github.com/')) {
284
+ cloneUrl = repoUrl.replace('https://github.com/', `https://${pat}@github.com/`);
285
+ cloneArgs[3] = cloneUrl;
286
+ }
287
+ execFileSync('git', cloneArgs, { stdio: 'pipe', timeout: 120_000 });
288
+
289
+ // Create sandbox
290
+ this.createSandbox(importId, repoUrl, targetPath);
291
+
292
+ // Take snapshot
293
+ this.takeSnapshot(targetPath);
294
+
295
+ // Detect stack
296
+ const stackInfo = this.detectStack(targetPath);
297
+
298
+ // Save manifest
299
+ const manifest = this.imports.get(importId);
300
+ manifest.owner = owner;
301
+ manifest.repo = repo;
302
+ manifest.name = repo;
303
+ manifest.stackInfo = stackInfo;
304
+ this._saveManifest(manifest);
305
+
306
+ this.daemon.audit?.log('repo.import', { importId, repoUrl, targetPath });
307
+
308
+ return { importId, path: targetPath, stackInfo };
309
+ }
310
+
311
+ createSandbox(importId, repoUrl, targetPath) {
312
+ const grooveDir = resolve(targetPath, '.groove');
313
+ mkdirSync(grooveDir, { recursive: true });
314
+
315
+ const manifest = {
316
+ id: importId,
317
+ repoUrl,
318
+ clonedTo: targetPath,
319
+ clonedAt: new Date().toISOString(),
320
+ status: 'active',
321
+ teamId: null,
322
+ agents: [],
323
+ processes: [],
324
+ configsModified: [],
325
+ globalInstalls: [],
326
+ dockerContainers: [],
327
+ credentialKeys: [],
328
+ };
329
+
330
+ writeFileSync(resolve(grooveDir, 'sandbox.json'), JSON.stringify(manifest, null, 2));
331
+ this._saveManifest(manifest);
332
+ }
333
+
334
+ takeSnapshot(targetPath) {
335
+ const snapshotDir = resolve(targetPath, '.groove', 'snapshot');
336
+ mkdirSync(snapshotDir, { recursive: true });
337
+
338
+ const projectDir = this.daemon.projectDir;
339
+ const filesToSnapshot = ['package.json', '.mcp.json', 'CLAUDE.md'];
340
+ for (const f of filesToSnapshot) {
341
+ const src = resolve(projectDir, f);
342
+ if (existsSync(src)) {
343
+ copyFileSync(src, resolve(snapshotDir, f));
344
+ }
345
+ }
346
+ }
347
+
348
+ recordProcess(importId, pid, command) {
349
+ const manifest = this.imports.get(importId);
350
+ if (!manifest) throw new Error(`Import not found: ${importId}`);
351
+ manifest.processes.push({ pid, command, startedAt: new Date().toISOString() });
352
+ this._saveManifest(manifest);
353
+ }
354
+
355
+ recordConfigChange(importId, filePath, originalContent) {
356
+ const manifest = this.imports.get(importId);
357
+ if (!manifest) throw new Error(`Import not found: ${importId}`);
358
+
359
+ const snapshotDir = resolve(manifest.clonedTo, '.groove', 'snapshot');
360
+ mkdirSync(snapshotDir, { recursive: true });
361
+ const safeName = basename(filePath).replace(/[^a-zA-Z0-9._-]/g, '_');
362
+ writeFileSync(resolve(snapshotDir, `config-${safeName}`), originalContent);
363
+
364
+ manifest.configsModified.push(filePath);
365
+ this._saveManifest(manifest);
366
+ }
367
+
368
+ // --- Removal ---
369
+
370
+ async softRemove(importId) {
371
+ const manifest = this.imports.get(importId);
372
+ if (!manifest) throw new Error(`Import not found: ${importId}`);
373
+
374
+ // Kill agents listed in manifest
375
+ for (const agentId of manifest.agents) {
376
+ try { await this.daemon.processes.kill(agentId); } catch { /* already dead */ }
377
+ }
378
+
379
+ // Kill tracked processes
380
+ for (const proc of manifest.processes) {
381
+ try { process.kill(proc.pid, 'SIGTERM'); } catch { /* already dead */ }
382
+ }
383
+
384
+ // Remove credential keys
385
+ if (this.daemon.credentials) {
386
+ for (const key of manifest.credentialKeys) {
387
+ try { this.daemon.credentials.deleteKey(key); } catch { /* ignore */ }
388
+ }
389
+ }
390
+
391
+ // Delete team
392
+ if (manifest.teamId) {
393
+ try { this.daemon.teams.delete(manifest.teamId); } catch { /* ignore */ }
394
+ }
395
+
396
+ // Remove .groove dir inside repo (if repo still exists)
397
+ const repoGrooveDir = resolve(manifest.clonedTo, '.groove');
398
+ if (existsSync(repoGrooveDir)) {
399
+ rmSync(repoGrooveDir, { recursive: true, force: true });
400
+ }
401
+
402
+ manifest.status = 'removed';
403
+ this._saveManifest(manifest);
404
+
405
+ this.daemon.audit?.log('repo.softRemove', { importId, clonedTo: manifest.clonedTo });
406
+ }
407
+
408
+ async hardNuke(importId, { deleteFiles = true } = {}) {
409
+ const manifest = this.imports.get(importId);
410
+ if (!manifest) throw new Error(`Import not found: ${importId}`);
411
+
412
+ // Revert config modifications from snapshot BEFORE softRemove deletes .groove/
413
+ const snapshotDir = resolve(manifest.clonedTo, '.groove', 'snapshot');
414
+ for (const filePath of manifest.configsModified) {
415
+ const safeName = basename(filePath).replace(/[^a-zA-Z0-9._-]/g, '_');
416
+ const snapshotPath = resolve(snapshotDir, `config-${safeName}`);
417
+ if (existsSync(snapshotPath)) {
418
+ try {
419
+ const original = readFileSync(snapshotPath, 'utf8');
420
+ writeFileSync(filePath, original);
421
+ } catch { /* best effort */ }
422
+ }
423
+ }
424
+
425
+ // Soft remove (kills agents, processes, credentials, team, removes .groove/)
426
+ if (manifest.status !== 'removed') {
427
+ await this.softRemove(importId);
428
+ }
429
+
430
+ if (!deleteFiles) {
431
+ this.daemon.audit?.log('repo.hardNuke', { importId, clonedTo: manifest.clonedTo, deleteFiles: false });
432
+ return;
433
+ }
434
+
435
+ // Rogue process scan
436
+ if (existsSync(manifest.clonedTo)) {
437
+ try {
438
+ const lsofOutput = execFileSync('lsof', ['+D', manifest.clonedTo], {
439
+ stdio: 'pipe', timeout: 10_000,
440
+ }).toString();
441
+ const pids = new Set();
442
+ for (const line of lsofOutput.split('\n').slice(1)) {
443
+ const parts = line.trim().split(/\s+/);
444
+ if (parts[1]) pids.add(parseInt(parts[1], 10));
445
+ }
446
+ for (const pid of pids) {
447
+ if (pid && !isNaN(pid)) {
448
+ try { process.kill(pid, 'SIGTERM'); } catch { /* ignore */ }
449
+ }
450
+ }
451
+ } catch { /* lsof may not find anything — that's fine */ }
452
+ }
453
+
454
+ // Delete repo directory
455
+ if (existsSync(manifest.clonedTo)) {
456
+ rmSync(manifest.clonedTo, { recursive: true, force: true });
457
+ }
458
+
459
+ // Remove import record
460
+ const recordPath = resolve(this.importsDir, `${importId}.json`);
461
+ if (existsSync(recordPath)) {
462
+ rmSync(recordPath);
463
+ }
464
+ this.imports.delete(importId);
465
+
466
+ this.daemon.audit?.log('repo.hardNuke', { importId, clonedTo: manifest.clonedTo });
467
+ }
468
+
469
+ // --- Queries ---
470
+
471
+ getImported() {
472
+ return Array.from(this.imports.values());
473
+ }
474
+
475
+ getImport(importId) {
476
+ return this.imports.get(importId) || null;
477
+ }
478
+
479
+ // --- Setup Agent Prompt ---
480
+
481
+ generateSetupPrompt(repoPath, stackInfo, readmeContent) {
482
+ const lines = [
483
+ `You are setting up an imported repository at: ${repoPath}`,
484
+ '',
485
+ `## Detected Stack`,
486
+ `- Runtime: ${stackInfo.runtime || 'unknown'}`,
487
+ `- Language: ${stackInfo.language || 'unknown'}`,
488
+ `- Package Manager: ${stackInfo.packageManager || 'unknown'}`,
489
+ `- Docker: ${stackInfo.hasDocker ? 'yes' : 'no'}`,
490
+ `- .env.example: ${stackInfo.hasEnvExample ? 'yes' : 'no'}`,
491
+ ];
492
+
493
+ if (stackInfo.installCommand) lines.push(`- Install: ${stackInfo.installCommand}`);
494
+ if (stackInfo.buildCommand) lines.push(`- Build: ${stackInfo.buildCommand}`);
495
+ if (stackInfo.testCommand) lines.push(`- Test: ${stackInfo.testCommand}`);
496
+
497
+ if (stackInfo.envVars && stackInfo.envVars.length > 0) {
498
+ lines.push('', '## Environment Variables (from .env.example)');
499
+ for (const v of stackInfo.envVars) {
500
+ lines.push(`- ${v.key}${v.hint ? ` — ${v.hint}` : ''}`);
501
+ }
502
+ }
503
+
504
+ if (readmeContent) {
505
+ const truncated = readmeContent.slice(0, 4000);
506
+ lines.push('', '## README Content', '', truncated);
507
+ }
508
+
509
+ lines.push(
510
+ '',
511
+ '## Playbook',
512
+ '1. Read the README and any setup docs — understand what this project does',
513
+ `2. Install dependencies: ${stackInfo.installCommand || '(detect and run)'}`,
514
+ '3. Check for .env.example — list what the user needs to provide',
515
+ '4. Run setup/init commands if documented',
516
+ '5. Run a build or type-check to verify setup is healthy',
517
+ '6. Summarize: what it is, what it does, how to use it, what is missing',
518
+ '7. Offer to create a team if the user wants ongoing work',
519
+ '',
520
+ '## Safety Rules (MUST FOLLOW)',
521
+ `- Do NOT install anything globally (no npm install -g, pip install --user, brew install)`,
522
+ `- Do NOT modify files outside ${repoPath}/`,
523
+ `- Do NOT run docker commands without asking the user first`,
524
+ `- Do NOT run destructive commands (rm -rf, drop database, etc.)`,
525
+ `- Track every process you spawn via: curl -s -X POST http://localhost:31415/api/repos/<importId>/process -H 'Content-Type: application/json' -d '{"pid":"<PID>","command":"<CMD>"}'`,
526
+ `- If something fails, report the error — do NOT retry destructively`,
527
+ );
528
+
529
+ return lines.join('\n');
530
+ }
531
+
532
+ // --- Helpers ---
533
+
534
+ _getPat() {
535
+ try {
536
+ const creds = this.daemon.credentials;
537
+ if (!creds) return null;
538
+ return creds.getKey?.('github') || creds.getKey?.('github-pat') || null;
539
+ } catch { return null; }
540
+ }
541
+ }
@@ -6,6 +6,7 @@ import { readFileSync, writeFileSync, existsSync } from 'fs';
6
6
  import { resolve } from 'path';
7
7
 
8
8
  const DEFAULT_THRESHOLD = 0.75;
9
+ const HARD_CEILING = 0.85; // Force rotate at 85% — no idle check, prevents compaction
9
10
  const CHECK_INTERVAL = 15_000;
10
11
  const QUALITY_THRESHOLD = 40; // Score below this triggers quality rotation
11
12
  const MIN_EVENTS = 10; // Minimum classifier events before scoring
@@ -130,6 +131,13 @@ export class Rotator extends EventEmitter {
130
131
  for (const agent of running) {
131
132
  if (this.rotating.has(agent.id)) continue;
132
133
 
134
+ // Hard ceiling — force rotate to prevent compaction, even if agent is busy
135
+ if (agent.contextUsage >= HARD_CEILING) {
136
+ console.log(` Rotator: ${agent.name} at ${Math.round(agent.contextUsage * 100)}% — FORCE rotating (hard ceiling)`);
137
+ await this.rotate(agent.id, { reason: 'hard_ceiling' });
138
+ continue;
139
+ }
140
+
133
141
  const threshold = this.daemon.adaptive
134
142
  ? this.daemon.adaptive.getThreshold(agent.provider, agent.role)
135
143
  : DEFAULT_THRESHOLD;
@@ -185,12 +193,29 @@ export class Rotator extends EventEmitter {
185
193
  delete this.liveScores[agentId];
186
194
  delete this.scoreHistory[agentId];
187
195
 
188
- let brief = await journalist.generateHandoffBrief(agent);
196
+ let brief = await journalist.generateHandoffBrief(agent, {
197
+ reason: options.reason,
198
+ qualityScore: options.qualityScore,
199
+ signals: options.signals,
200
+ });
189
201
 
190
202
  if (options.additionalPrompt) {
191
203
  brief = brief + '\n\n## User Instruction\n\n' + options.additionalPrompt;
192
204
  }
193
205
 
206
+ // Persist to Layer 7 handoff chain so future rotations have causal continuity
207
+ if (this.daemon.memory) {
208
+ this.daemon.memory.appendHandoffBrief(agent.role, {
209
+ timestamp: new Date().toISOString(),
210
+ agentId: agent.id,
211
+ newAgentId: null, // filled after respawn completes
212
+ reason: options.reason || 'manual',
213
+ oldTokens: agent.tokensUsed,
214
+ contextUsage: agent.contextUsage,
215
+ brief: brief.slice(0, 4000),
216
+ });
217
+ }
218
+
194
219
  const record = {
195
220
  agentId: agent.id,
196
221
  agentName: agent.name,
@@ -332,6 +357,7 @@ export class Rotator extends EventEmitter {
332
357
  const qualityRotations = this.rotationHistory.filter((r) => r.reason === 'quality_degradation').length;
333
358
  const contextRotations = this.rotationHistory.filter((r) => r.reason === 'context_threshold').length;
334
359
  const naturalCompactions = this.rotationHistory.filter((r) => r.reason === 'natural_compaction').length;
360
+ const hardCeilingRotations = this.rotationHistory.filter((r) => r.reason === 'hard_ceiling').length;
335
361
  return {
336
362
  enabled: this.enabled,
337
363
  totalRotations,
@@ -339,6 +365,7 @@ export class Rotator extends EventEmitter {
339
365
  qualityRotations,
340
366
  contextRotations,
341
367
  naturalCompactions,
368
+ hardCeilingRotations,
342
369
  rotating: Array.from(this.rotating),
343
370
  liveScores: this.liveScores,
344
371
  scoreHistory: this.scoreHistory,
@@ -18,7 +18,7 @@ export class Supervisor {
18
18
 
19
19
  // --- Approval System ---
20
20
 
21
- requestApproval(agentId, action) {
21
+ requestApproval(agentId, action, retryPayload = null) {
22
22
  const id = `approval-${Date.now()}-${++this.counter}`;
23
23
  const agent = this.daemon.registry?.get(agentId);
24
24
 
@@ -30,6 +30,7 @@ export class Supervisor {
30
30
  status: 'pending',
31
31
  requestedAt: new Date().toISOString(),
32
32
  };
33
+ if (retryPayload) approval.retryPayload = retryPayload;
33
34
 
34
35
  this.pendingApprovals.set(id, approval);
35
36
  this.daemon.broadcast({ type: 'approval:request', data: approval });