groove-dev 0.27.100 → 0.27.102
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/CLAUDE.md +7 -0
- 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 +33 -7
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +2 -2
- package/node_modules/@groove-dev/daemon/src/preview.js +148 -2
- package/node_modules/@groove-dev/daemon/src/process.js +34 -2
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +10 -4
- package/node_modules/@groove-dev/gui/dist/assets/{index-CuFxAnNE.js → index-8gdXdRnq.js} +1743 -1743
- package/node_modules/@groove-dev/gui/dist/assets/index-C1ObKizg.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +199 -14
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +19 -6
- 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 +33 -7
- package/packages/daemon/src/gateways/manager.js +2 -2
- package/packages/daemon/src/preview.js +148 -2
- package/packages/daemon/src/process.js +34 -2
- package/packages/daemon/src/providers/gemini.js +10 -4
- package/packages/gui/dist/assets/{index-CuFxAnNE.js → index-8gdXdRnq.js} +1743 -1743
- package/packages/gui/dist/assets/index-C1ObKizg.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-file-tree.jsx +199 -14
- package/packages/gui/src/components/agents/workspace-mode.jsx +19 -6
- package/packages/launch-page/dist/assets/index-Bo186ysq.js +4180 -0
- package/packages/launch-page/dist/assets/index-CP4c4yxe.css +1 -0
- package/packages/launch-page/dist/index.html +2 -2
- package/packages/launch-page/src/App.css +438 -137
- package/packages/launch-page/src/App.tsx +171 -123
- package/packages/launch-page/src/index.css +9 -2
- package/node_modules/@groove-dev/gui/dist/assets/index-BvAGbs8U.css +0 -1
- package/packages/gui/dist/assets/index-BvAGbs8U.css +0 -1
- package/packages/launch-page/dist/assets/index-BK3nAvHG.js +0 -4180
- package/packages/launch-page/dist/assets/index-jrLVZW5U.css +0 -2
package/CLAUDE.md
CHANGED
|
@@ -263,3 +263,10 @@ Audit-driven release. Multi-agent orchestration system with 7 coordination layer
|
|
|
263
263
|
- Dashboard: routing donut, cache panel, context health gauges
|
|
264
264
|
- Monitor/QC agent mode (stay active, loop)
|
|
265
265
|
- Distribution: demo video, HN launch, Twitter content
|
|
266
|
+
|
|
267
|
+
<!-- GROOVE:START -->
|
|
268
|
+
## GROOVE Orchestration (auto-injected)
|
|
269
|
+
Active agents: 0
|
|
270
|
+
See AGENTS_REGISTRY.md for full agent state.
|
|
271
|
+
**Memory policy:** GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.
|
|
272
|
+
<!-- GROOVE:END -->
|
|
@@ -174,10 +174,10 @@ export function createApi(app, daemon) {
|
|
|
174
174
|
const agent = daemon.registry.get(req.params.id);
|
|
175
175
|
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
// Always attempt kill — handles race where GUI sees 'running' but daemon
|
|
178
|
+
// already marked the agent completed (common with fast non-interactive
|
|
179
|
+
// providers like Gemini). processes.kill() is a no-op when no handle exists.
|
|
180
|
+
await daemon.processes.kill(req.params.id);
|
|
181
181
|
|
|
182
182
|
// Only purge from registry when explicitly requested.
|
|
183
183
|
// Killed/completed agents stay visible so the user can review output.
|
|
@@ -1519,6 +1519,30 @@ export function createApi(app, daemon) {
|
|
|
1519
1519
|
return res.json(newAgent);
|
|
1520
1520
|
}
|
|
1521
1521
|
|
|
1522
|
+
// Non-interactive CLI providers (e.g. Gemini): respawn with the new
|
|
1523
|
+
// message as the prompt, preserving original introContext. These providers
|
|
1524
|
+
// run one prompt per spawn and cannot resume sessions.
|
|
1525
|
+
if (provider?.constructor?.nonInteractive && !daemon.processes.isRunning(req.params.id)) {
|
|
1526
|
+
const oldConfig = { ...agent };
|
|
1527
|
+
daemon.registry.remove(req.params.id);
|
|
1528
|
+
daemon.locks.release(req.params.id);
|
|
1529
|
+
|
|
1530
|
+
const newAgent = await daemon.processes.spawn({
|
|
1531
|
+
role: oldConfig.role,
|
|
1532
|
+
scope: oldConfig.scope,
|
|
1533
|
+
provider: oldConfig.provider,
|
|
1534
|
+
model: oldConfig.model,
|
|
1535
|
+
prompt: message.trim(),
|
|
1536
|
+
introContext: oldConfig.introContext,
|
|
1537
|
+
permission: oldConfig.permission || 'full',
|
|
1538
|
+
workingDir: oldConfig.workingDir,
|
|
1539
|
+
name: oldConfig.name,
|
|
1540
|
+
teamId: oldConfig.teamId,
|
|
1541
|
+
});
|
|
1542
|
+
daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed: false });
|
|
1543
|
+
return res.json(newAgent);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1522
1546
|
// Running CLI agent (no loop) — queue the message for delivery after
|
|
1523
1547
|
// the current task completes instead of killing and respawning.
|
|
1524
1548
|
if (daemon.processes.isRunning(req.params.id)) {
|
|
@@ -3364,6 +3388,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3364
3388
|
// Resolve base directory from the planner that wrote the file, not the daemon root
|
|
3365
3389
|
const plannerAgent = found.agentId ? daemon.registry.get(found.agentId) : null;
|
|
3366
3390
|
const baseDir = plannerAgent?.workingDir || daemon.config?.defaultWorkingDir || daemon.projectDir;
|
|
3391
|
+
const plannerProvider = plannerAgent?.provider || undefined;
|
|
3367
3392
|
|
|
3368
3393
|
// Use the planner's teamId so launched agents join the correct team.
|
|
3369
3394
|
// Priority: explicit from frontend > agent that wrote the file > most recent planner > default
|
|
@@ -3424,6 +3449,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3424
3449
|
phase2 = [{
|
|
3425
3450
|
name: 'qc-agent',
|
|
3426
3451
|
role: 'fullstack', phase: 2, scope: [],
|
|
3452
|
+
provider: teamProvider || plannerProvider || daemon.config?.defaultProvider || undefined,
|
|
3427
3453
|
prompt: 'QC Senior Dev: All builder agents have completed. Audit their changes for correctness, fix any issues, run tests, and verify the project builds cleanly (npm run build). Do NOT start long-running dev servers — just verify the build succeeds. Commit all changes. IMPORTANT: Do NOT delete files from other projects or directories outside this project.',
|
|
3428
3454
|
}];
|
|
3429
3455
|
}
|
|
@@ -3476,7 +3502,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3476
3502
|
role: existing.role,
|
|
3477
3503
|
scope: normalizeScope(config.scope || existing.scope || [], existing.workingDir || projectWorkingDir),
|
|
3478
3504
|
prompt,
|
|
3479
|
-
provider: config.provider || daemon.config?.defaultProvider || existing.provider || undefined,
|
|
3505
|
+
provider: config.provider || plannerProvider || daemon.config?.defaultProvider || existing.provider || undefined,
|
|
3480
3506
|
model: config.model || existing.model || daemon.config?.defaultModel || 'auto',
|
|
3481
3507
|
permission: config.permission || existing.permission || 'auto',
|
|
3482
3508
|
workingDir: existing.workingDir || projectWorkingDir,
|
|
@@ -3501,7 +3527,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3501
3527
|
role: config.role,
|
|
3502
3528
|
scope: normalizeScope(config.scope || [], config.workingDir || projectWorkingDir),
|
|
3503
3529
|
prompt,
|
|
3504
|
-
provider: config.provider || daemon.config?.defaultProvider || undefined,
|
|
3530
|
+
provider: config.provider || plannerProvider || daemon.config?.defaultProvider || undefined,
|
|
3505
3531
|
model: config.model || daemon.config?.defaultModel || 'auto',
|
|
3506
3532
|
permission: config.permission || 'auto',
|
|
3507
3533
|
workingDir: config.workingDir || projectWorkingDir,
|
|
@@ -3544,7 +3570,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3544
3570
|
waitFor: phase1Ids,
|
|
3545
3571
|
agents: phase2.map((c) => ({
|
|
3546
3572
|
role: c.role, scope: c.scope || [], prompt: c.prompt || '',
|
|
3547
|
-
provider: c.provider || daemon.config?.defaultProvider || undefined, model: c.model || daemon.config?.defaultModel || 'auto',
|
|
3573
|
+
provider: c.provider || plannerProvider || daemon.config?.defaultProvider || undefined, model: c.model || daemon.config?.defaultModel || 'auto',
|
|
3548
3574
|
permission: c.permission || 'auto',
|
|
3549
3575
|
reasoningEffort: c.reasoningEffort, temperature: c.temperature, verbosity: c.verbosity,
|
|
3550
3576
|
workingDir: c.workingDir || projectWorkingDir,
|
|
@@ -799,7 +799,7 @@ export class GatewayManager {
|
|
|
799
799
|
role: config.role,
|
|
800
800
|
scope: config.scope || [],
|
|
801
801
|
prompt: config.prompt || '',
|
|
802
|
-
provider: config.provider || 'claude-code',
|
|
802
|
+
provider: config.provider || this.daemon.config?.defaultProvider || 'claude-code',
|
|
803
803
|
model: config.model || 'auto',
|
|
804
804
|
permission: config.permission || 'auto',
|
|
805
805
|
workingDir: config.workingDir || defaultDir,
|
|
@@ -833,7 +833,7 @@ export class GatewayManager {
|
|
|
833
833
|
waitFor: phase1Ids,
|
|
834
834
|
agents: phase2.map((c) => ({
|
|
835
835
|
role: c.role, scope: c.scope || [], prompt: c.prompt || '',
|
|
836
|
-
provider: c.provider || 'claude-code', model: c.model || 'auto',
|
|
836
|
+
provider: c.provider || this.daemon.config?.defaultProvider || 'claude-code', model: c.model || 'auto',
|
|
837
837
|
permission: c.permission || 'auto',
|
|
838
838
|
workingDir: c.workingDir || defaultDir,
|
|
839
839
|
name: c.name || undefined,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// kills the previous one. Previews are also killed on team delete and on
|
|
12
12
|
// daemon shutdown.
|
|
13
13
|
|
|
14
|
-
import { spawn as cpSpawn } from 'child_process';
|
|
14
|
+
import { spawn as cpSpawn, execSync } from 'child_process';
|
|
15
15
|
import { resolve, extname } from 'path';
|
|
16
16
|
import { existsSync, readFileSync, statSync } from 'fs';
|
|
17
17
|
import { createServer } from 'http';
|
|
@@ -101,11 +101,55 @@ export class PreviewService {
|
|
|
101
101
|
return result;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
const installResult = this._ensureDependencies(teamId, baseDir);
|
|
105
|
+
if (installResult?.failed) {
|
|
106
|
+
this.daemon.audit?.log('preview.failed', { teamId, reason: installResult.reason });
|
|
107
|
+
return { launched: false, reason: installResult.reason };
|
|
108
|
+
}
|
|
109
|
+
|
|
104
110
|
let result;
|
|
105
111
|
if (preview.kind === 'static-html') {
|
|
106
|
-
|
|
112
|
+
if (this._needsBuild(baseDir, preview)) {
|
|
113
|
+
const buildResult = this._runBuild(teamId, baseDir);
|
|
114
|
+
if (buildResult?.failed) {
|
|
115
|
+
this.daemon.audit?.log('preview.failed', { teamId, reason: buildResult.reason });
|
|
116
|
+
return { launched: false, reason: buildResult.reason };
|
|
117
|
+
}
|
|
118
|
+
const distDir = resolve(baseDir, 'dist');
|
|
119
|
+
if (existsSync(distDir)) {
|
|
120
|
+
result = await this._launchStatic(teamId, distDir, { ...preview, openPath: preview.openPath || 'index.html' });
|
|
121
|
+
} else {
|
|
122
|
+
result = await this._launchStatic(teamId, baseDir, preview);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
result = await this._launchStatic(teamId, baseDir, preview);
|
|
126
|
+
}
|
|
107
127
|
} else if (preview.kind === 'dev-server') {
|
|
128
|
+
if (this._needsPreBuild(baseDir)) {
|
|
129
|
+
const preBuild = this._runBuild(teamId, baseDir);
|
|
130
|
+
if (preBuild?.failed) {
|
|
131
|
+
this.daemon.audit?.log('preview.prebuild-failed', { teamId, reason: preBuild.reason });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
108
134
|
result = await this._launchDevServer(teamId, baseDir, preview);
|
|
135
|
+
// Fallback: if dev-server failed, try building and serving statically
|
|
136
|
+
if (!result.launched) {
|
|
137
|
+
const pkgPath = resolve(baseDir, 'package.json');
|
|
138
|
+
if (existsSync(pkgPath)) {
|
|
139
|
+
try {
|
|
140
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
141
|
+
if (pkg.scripts?.build) {
|
|
142
|
+
const buildResult = this._runBuild(teamId, baseDir);
|
|
143
|
+
if (!buildResult?.failed) {
|
|
144
|
+
const distDir = resolve(baseDir, 'dist');
|
|
145
|
+
if (existsSync(resolve(distDir, 'index.html'))) {
|
|
146
|
+
result = await this._launchStatic(teamId, distDir, { ...preview, openPath: 'index.html' });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch { /* fallback failed, keep original error */ }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
109
153
|
} else {
|
|
110
154
|
result = { launched: false, reason: `unknown_kind: ${preview.kind}` };
|
|
111
155
|
}
|
|
@@ -118,6 +162,99 @@ export class PreviewService {
|
|
|
118
162
|
return result;
|
|
119
163
|
}
|
|
120
164
|
|
|
165
|
+
_ensureDependencies(teamId, baseDir) {
|
|
166
|
+
const pkgPath = resolve(baseDir, 'package.json');
|
|
167
|
+
const nodeModules = resolve(baseDir, 'node_modules');
|
|
168
|
+
if (!existsSync(pkgPath) || existsSync(nodeModules)) return null;
|
|
169
|
+
try {
|
|
170
|
+
console.log(`[Groove:Preview] Running npm install in ${baseDir}`);
|
|
171
|
+
this.daemon.audit?.log('preview.npm-install', { teamId, baseDir });
|
|
172
|
+
execSync('npm install', { cwd: baseDir, timeout: 120_000, stdio: 'pipe' });
|
|
173
|
+
return null;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return { failed: true, reason: `npm install failed: ${err.message?.slice(0, 300)}` };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_needsBuild(baseDir, preview) {
|
|
180
|
+
const pkgPath = resolve(baseDir, 'package.json');
|
|
181
|
+
let hasBuildScript = false;
|
|
182
|
+
try {
|
|
183
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
184
|
+
hasBuildScript = !!pkg.scripts?.build;
|
|
185
|
+
} catch { /* no package.json or malformed */ }
|
|
186
|
+
|
|
187
|
+
const distDir = resolve(baseDir, 'dist');
|
|
188
|
+
const distExists = existsSync(distDir);
|
|
189
|
+
|
|
190
|
+
// Primary: build script exists and dist/ doesn't
|
|
191
|
+
if (hasBuildScript && !distExists) return true;
|
|
192
|
+
|
|
193
|
+
// Stale check: dist/ exists but package.json is newer than dist/index.html
|
|
194
|
+
if (hasBuildScript && distExists) {
|
|
195
|
+
const distIndex = resolve(distDir, 'index.html');
|
|
196
|
+
if (existsSync(distIndex) && existsSync(pkgPath)) {
|
|
197
|
+
try {
|
|
198
|
+
const distMtime = statSync(distIndex).mtimeMs;
|
|
199
|
+
const pkgMtime = statSync(pkgPath).mtimeMs;
|
|
200
|
+
if (pkgMtime > distMtime) return true;
|
|
201
|
+
} catch { /* ignore stat errors */ }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Secondary: entry file references .tsx/.jsx sources (needs transpilation)
|
|
206
|
+
const openPath = (preview.openPath || 'index.html').replace(/^\/+/, '');
|
|
207
|
+
const entryFile = resolve(baseDir, openPath);
|
|
208
|
+
if (existsSync(entryFile)) {
|
|
209
|
+
try {
|
|
210
|
+
const html = readFileSync(entryFile, 'utf8');
|
|
211
|
+
if (/src=["'][^"']*\.(tsx?|jsx)["']/i.test(html)) return true;
|
|
212
|
+
} catch { /* ignore */ }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Entry file missing — check if a build might create it
|
|
216
|
+
if (!existsSync(entryFile) && hasBuildScript) {
|
|
217
|
+
const frameworkConfigs = ['vite.config', 'next.config', 'webpack.config'];
|
|
218
|
+
for (const cfg of frameworkConfigs) {
|
|
219
|
+
for (const ext of ['.js', '.ts', '.mjs', '.cjs']) {
|
|
220
|
+
if (existsSync(resolve(baseDir, cfg + ext))) return true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_needsPreBuild(baseDir) {
|
|
229
|
+
const pkgPath = resolve(baseDir, 'package.json');
|
|
230
|
+
if (!existsSync(pkgPath)) return false;
|
|
231
|
+
try {
|
|
232
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
233
|
+
const startScript = pkg.scripts?.start || '';
|
|
234
|
+
if (/\bnext\s+start\b/.test(startScript)) return true;
|
|
235
|
+
if (/\bserve\b/.test(startScript) && !pkg.scripts?.dev) return true;
|
|
236
|
+
if (/\bhttp-server\b/.test(startScript)) return true;
|
|
237
|
+
} catch { /* ignore */ }
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_runBuild(teamId, baseDir) {
|
|
242
|
+
const pkgPath = resolve(baseDir, 'package.json');
|
|
243
|
+
if (!existsSync(pkgPath)) return { failed: true, reason: 'no package.json for build' };
|
|
244
|
+
try {
|
|
245
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
246
|
+
if (!pkg.scripts?.build) return { failed: true, reason: 'no build script' };
|
|
247
|
+
} catch { return { failed: true, reason: 'malformed package.json' }; }
|
|
248
|
+
try {
|
|
249
|
+
console.log(`[Groove:Preview] Running npm run build in ${baseDir}`);
|
|
250
|
+
this.daemon.audit?.log('preview.build', { teamId, baseDir });
|
|
251
|
+
execSync('npm run build', { cwd: baseDir, timeout: 120_000, stdio: 'pipe' });
|
|
252
|
+
return null;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return { failed: true, reason: `build failed: ${err.message?.slice(0, 300)}` };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
121
258
|
_launchStatic(teamId, baseDir, preview) {
|
|
122
259
|
const openPath = (preview.openPath || 'index.html').replace(/^\/+/, '');
|
|
123
260
|
const entryFile = resolve(baseDir, openPath);
|
|
@@ -130,6 +267,15 @@ export class PreviewService {
|
|
|
130
267
|
const filePath = resolve(baseDir, rel);
|
|
131
268
|
if (!filePath.startsWith(baseDir)) { res.statusCode = 403; return res.end(); }
|
|
132
269
|
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
|
|
270
|
+
// SPA fallback: serve index.html for HTML requests (client-side routing)
|
|
271
|
+
const acceptsHtml = (req.headers.accept || '').includes('text/html');
|
|
272
|
+
if (acceptsHtml) {
|
|
273
|
+
const fallback = resolve(baseDir, openPath);
|
|
274
|
+
if (existsSync(fallback) && statSync(fallback).isFile()) {
|
|
275
|
+
res.setHeader('Content-Type', 'text/html');
|
|
276
|
+
return res.end(readFileSync(fallback));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
133
279
|
res.statusCode = 404; return res.end('Not found');
|
|
134
280
|
}
|
|
135
281
|
res.setHeader('Content-Type', mimeLookup(extname(filePath)) || 'application/octet-stream');
|
|
@@ -741,6 +741,10 @@ For normal file edits within your scope, proceed without review.
|
|
|
741
741
|
// Clean up per-agent maps to prevent unbounded growth in long sessions
|
|
742
742
|
this.peakContextUsage.delete(agent.id);
|
|
743
743
|
this.pendingMessages.delete(agent.id);
|
|
744
|
+
|
|
745
|
+
// Release file-scope locks so they don't persist after agent death
|
|
746
|
+
if (this.daemon.locks) this.daemon.locks.release(agent.id);
|
|
747
|
+
|
|
744
748
|
registry.update(agent.id, { status, pid: null });
|
|
745
749
|
|
|
746
750
|
if (this.daemon.timeline) {
|
|
@@ -1351,6 +1355,13 @@ For normal file edits within your scope, proceed without review.
|
|
|
1351
1355
|
if (block.type === 'text') textParts.push(block.text);
|
|
1352
1356
|
}
|
|
1353
1357
|
}
|
|
1358
|
+
if (evt.type === 'message' && evt.content) {
|
|
1359
|
+
const parts = Array.isArray(evt.content) ? evt.content : [evt.content];
|
|
1360
|
+
for (const p of parts) {
|
|
1361
|
+
if (typeof p === 'string') textParts.push(p);
|
|
1362
|
+
else if (p.text) textParts.push(p.text);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1354
1365
|
} catch { textParts.push(line); }
|
|
1355
1366
|
}
|
|
1356
1367
|
const fullText = textParts.join('\n');
|
|
@@ -1798,6 +1809,10 @@ For normal file edits within your scope, proceed without review.
|
|
|
1798
1809
|
logStream.end();
|
|
1799
1810
|
this.handles.delete(newAgent.id);
|
|
1800
1811
|
this._stalledAgents.delete(newAgent.id);
|
|
1812
|
+
|
|
1813
|
+
// Release file-scope locks so they don't persist after agent death
|
|
1814
|
+
if (this.daemon.locks) this.daemon.locks.release(newAgent.id);
|
|
1815
|
+
|
|
1801
1816
|
const finalStatus = signal === 'SIGTERM' || signal === 'SIGKILL' ? 'killed' : code === 0 ? 'completed' : 'crashed';
|
|
1802
1817
|
registry.update(newAgent.id, { status: finalStatus, pid: null });
|
|
1803
1818
|
this.daemon.broadcast({ type: 'agent:exit', agentId: newAgent.id, code, signal, status: finalStatus });
|
|
@@ -1929,6 +1944,10 @@ For normal file edits within your scope, proceed without review.
|
|
|
1929
1944
|
this._streamThrottle.delete(newAgent.id);
|
|
1930
1945
|
this.peakContextUsage.delete(newAgent.id);
|
|
1931
1946
|
this.pendingMessages.delete(newAgent.id);
|
|
1947
|
+
|
|
1948
|
+
// Release file-scope locks so they don't persist after agent death
|
|
1949
|
+
if (this.daemon.locks) this.daemon.locks.release(newAgent.id);
|
|
1950
|
+
|
|
1932
1951
|
registry.update(newAgent.id, { status, pid: null });
|
|
1933
1952
|
|
|
1934
1953
|
const agentData = registry.get(newAgent.id);
|
|
@@ -2077,14 +2096,26 @@ For normal file edits within your scope, proceed without review.
|
|
|
2077
2096
|
|
|
2078
2097
|
// CLI process path — spawn's exit handler sets status='killed' for SIGTERM
|
|
2079
2098
|
return new Promise((resolveKill) => {
|
|
2099
|
+
let resolved = false;
|
|
2100
|
+
const resolve = () => { if (!resolved) { resolved = true; resolveKill(); } };
|
|
2101
|
+
|
|
2080
2102
|
const forceTimer = setTimeout(() => {
|
|
2081
2103
|
try { proc.kill('SIGKILL'); } catch {}
|
|
2082
2104
|
}, 5000);
|
|
2083
2105
|
|
|
2106
|
+
// Hard timeout: resolve even if exit event never fires (prevents HTTP hang)
|
|
2107
|
+
const hardTimer = setTimeout(() => {
|
|
2108
|
+
clearTimeout(forceTimer);
|
|
2109
|
+
this.handles.delete(agentId);
|
|
2110
|
+
this.daemon.locks.release(agentId);
|
|
2111
|
+
resolve();
|
|
2112
|
+
}, 10000);
|
|
2113
|
+
|
|
2084
2114
|
proc.on('exit', () => {
|
|
2085
2115
|
clearTimeout(forceTimer);
|
|
2116
|
+
clearTimeout(hardTimer);
|
|
2086
2117
|
this.daemon.locks.release(agentId);
|
|
2087
|
-
|
|
2118
|
+
resolve();
|
|
2088
2119
|
});
|
|
2089
2120
|
|
|
2090
2121
|
try {
|
|
@@ -2092,9 +2123,10 @@ For normal file edits within your scope, proceed without review.
|
|
|
2092
2123
|
} catch {
|
|
2093
2124
|
// Already dead
|
|
2094
2125
|
clearTimeout(forceTimer);
|
|
2126
|
+
clearTimeout(hardTimer);
|
|
2095
2127
|
this.handles.delete(agentId);
|
|
2096
2128
|
this.daemon.locks.release(agentId);
|
|
2097
|
-
|
|
2129
|
+
resolve();
|
|
2098
2130
|
}
|
|
2099
2131
|
});
|
|
2100
2132
|
}
|
|
@@ -28,6 +28,7 @@ export class GeminiProvider extends Provider {
|
|
|
28
28
|
static name = 'gemini';
|
|
29
29
|
static displayName = 'Gemini CLI';
|
|
30
30
|
static command = 'gemini';
|
|
31
|
+
static nonInteractive = true;
|
|
31
32
|
static authType = 'api-key';
|
|
32
33
|
static envKey = 'GEMINI_API_KEY';
|
|
33
34
|
static models = [
|
|
@@ -59,7 +60,7 @@ export class GeminiProvider extends Provider {
|
|
|
59
60
|
|
|
60
61
|
args.push('--yolo');
|
|
61
62
|
args.push('--output-format', 'stream-json');
|
|
62
|
-
args.push('-p'
|
|
63
|
+
args.push('-p');
|
|
63
64
|
|
|
64
65
|
this._currentModel = agent.model;
|
|
65
66
|
|
|
@@ -84,8 +85,13 @@ export class GeminiProvider extends Provider {
|
|
|
84
85
|
if (agent.role !== 'planner') {
|
|
85
86
|
parts.push(
|
|
86
87
|
`## Non-Interactive Commands\n\n` +
|
|
87
|
-
`Always use non-interactive flags when running package manager commands to prevent timeout hangs
|
|
88
|
-
|
|
88
|
+
`Always use non-interactive flags when running package manager commands to prevent timeout hangs:\n` +
|
|
89
|
+
`- npx: always use \`npx --yes\`\n` +
|
|
90
|
+
`- create-vite: always add \`--no-interactive\` flag (e.g. \`npx --yes create-vite . --template react --no-interactive\`)\n` +
|
|
91
|
+
`- create-next-app: always add \`--yes\` flag\n` +
|
|
92
|
+
`- npm init/create: always add \`--yes\` flag\n` +
|
|
93
|
+
`- Any scaffolding tool: look for --no-interactive, --yes, or -y flags to skip prompts\n` +
|
|
94
|
+
`Never run these commands without non-interactive flags — they will hang and be auto-killed after 5 minutes.`
|
|
89
95
|
);
|
|
90
96
|
}
|
|
91
97
|
return parts.join('\n\n');
|
|
@@ -155,7 +161,7 @@ export class GeminiProvider extends Provider {
|
|
|
155
161
|
|
|
156
162
|
switch (event.type) {
|
|
157
163
|
case 'init':
|
|
158
|
-
return { type: 'activity', subtype: 'assistant',
|
|
164
|
+
return { type: 'activity', subtype: 'assistant', model: event.model, data: [{ type: 'text', text: '' }] };
|
|
159
165
|
|
|
160
166
|
case 'message': {
|
|
161
167
|
if (event.role === 'user') return null;
|