groove-dev 0.27.186 → 0.27.188

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 (42) hide show
  1. package/innerchat/Screenshot_2026-07-23_at_1.30.16_PM.png +0 -0
  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/index.js +6 -0
  5. package/node_modules/@groove-dev/daemon/src/innerchat-docs.js +34 -13
  6. package/node_modules/@groove-dev/daemon/src/introducer.js +2 -2
  7. package/node_modules/@groove-dev/daemon/src/process.js +13 -1
  8. package/node_modules/@groove-dev/daemon/src/teams.js +36 -4
  9. package/node_modules/@groove-dev/daemon/src/watcher.js +230 -104
  10. package/node_modules/@groove-dev/daemon/test/teams.test.js +48 -1
  11. package/node_modules/@groove-dev/daemon/test/watcher.test.js +68 -10
  12. package/node_modules/@groove-dev/gui/dist/assets/index-Cat5pJUx.css +1 -0
  13. package/node_modules/@groove-dev/gui/dist/assets/{index-DOOaCFRS.js → index-fyGUwOq4.js} +222 -222
  14. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  15. package/node_modules/@groove-dev/gui/package.json +1 -1
  16. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +31 -3
  17. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +106 -10
  18. package/node_modules/@groove-dev/gui/src/lib/logpaths.js +1 -1
  19. package/node_modules/@groove-dev/gui/src/stores/groove.js +6 -6
  20. package/node_modules/@groove-dev/gui/src/stores/helpers.js +28 -0
  21. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +20 -9
  22. package/package.json +1 -1
  23. package/packages/cli/package.json +1 -1
  24. package/packages/daemon/package.json +1 -1
  25. package/packages/daemon/src/index.js +6 -0
  26. package/packages/daemon/src/innerchat-docs.js +34 -13
  27. package/packages/daemon/src/introducer.js +2 -2
  28. package/packages/daemon/src/process.js +13 -1
  29. package/packages/daemon/src/teams.js +36 -4
  30. package/packages/daemon/src/watcher.js +230 -104
  31. package/packages/gui/dist/assets/index-Cat5pJUx.css +1 -0
  32. package/packages/gui/dist/assets/{index-DOOaCFRS.js → index-fyGUwOq4.js} +222 -222
  33. package/packages/gui/dist/index.html +2 -2
  34. package/packages/gui/package.json +1 -1
  35. package/packages/gui/src/components/agents/agent-feed.jsx +31 -3
  36. package/packages/gui/src/components/agents/agent-panel.jsx +106 -10
  37. package/packages/gui/src/lib/logpaths.js +1 -1
  38. package/packages/gui/src/stores/groove.js +6 -6
  39. package/packages/gui/src/stores/helpers.js +28 -0
  40. package/packages/gui/src/stores/slices/agents-slice.js +20 -9
  41. package/node_modules/@groove-dev/gui/dist/assets/index-CU8L_r5f.css +0 -1
  42. package/packages/gui/dist/assets/index-CU8L_r5f.css +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.186",
3
+ "version": "0.27.188",
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.27.186",
3
+ "version": "0.27.188",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -638,6 +638,12 @@ export class Daemon {
638
638
  }
639
639
  }
640
640
 
641
+ // Reconnect to watches whose detached jobs survived this restart, so a
642
+ // "notify me when the training finishes" watch still fires afterward.
643
+ try { this.watcher.restore(); } catch (err) {
644
+ console.error('[startup] Failed to restore watches:', err.message);
645
+ }
646
+
641
647
  // Restore auth token from stored config so subscription polling works after restart
642
648
  const storedToken = this.skills.getToken();
643
649
  if (storedToken) {
@@ -70,27 +70,48 @@ export function innerChatInstructions(port = 31415, agentName = 'YOUR_NAME') {
70
70
  export function watchInstructions(port = 31415, agentName = 'YOUR_NAME') {
71
71
  const nameHint = agentName === 'YOUR_NAME' ? ' ($GROOVE_AGENT_NAME)' : '';
72
72
  return [
73
- '## Getting Notified When Something Finishes (Watch)',
73
+ '## Running Long Jobs & Getting Notified (Watch)',
74
74
  '',
75
- 'Your process ends when your turn ends, so a test suite or long job you kicked',
76
- 'off has nothing to notify you telling the user "I\'ll report back when it lands"',
77
- 'does not work on its own; the session is gone. Instead, set a Watch. The daemon',
78
- 'outlives your turn and RESUMES you with the result when it finishes:',
75
+ '> **A job you start yourself dies when your turn ends.** Anything you launch in Bash ',
76
+ "> even backgrounded with `&`, `nohup`, or `disown` is a child of your session process.",
77
+ '> When your turn ends or your context rotates, that process is torn down and the job dies',
78
+ '> with it. Do NOT start a long-running job (training, build, server, benchmark) directly',
79
+ "> in Bash and expect it to keep running. It won't.",
80
+ '',
81
+ 'Instead, hand the command to a Watch. The DAEMON runs it fully detached — independent of',
82
+ 'your turn, surviving turn-end, context rotation, and even a daemon restart — and RESUMES',
83
+ 'you with the exit code and output when it finishes:',
79
84
  '',
80
85
  '```bash',
81
86
  `curl -s http://localhost:${port}/api/watch -X POST -H 'Content-Type: application/json' \\`,
82
- ` -d '{"agent":"${agentName}","label":"test suite","command":"npm test"}'`,
87
+ ` -d '{"agent":"${agentName}","label":"model training","command":"python train.py"}'`,
83
88
  '```',
84
89
  '',
85
- 'This returns immediately with a `watchId`. **End your turn** — you will be woken',
86
- 'with the exit code and output tail when the command finishes.',
90
+ 'This returns immediately with a `watchId`. **End your turn** — the job keeps running under',
91
+ 'the daemon and you will be woken with the result when it completes.',
87
92
  '',
88
- '- Prefer `command` (let the daemon run it) over launching it yourself that way',
89
- ' it captures the real exit code and output.',
90
- '- For something ALREADY running, poll instead with `until` (fires when the check',
91
- ` exits 0): \`-d '{"agent":"${agentName}","label":"server up","until":"curl -sf localhost:3000/health"}'\`.`,
93
+ '- Use `command` for anything that must outlive your turn. Let the daemon run it — do not',
94
+ ' launch it yourself and then watch it; a self-launched job is already doomed.',
95
+ '- Use `until` ONLY for something the daemon or another durable process is already running',
96
+ ` (not a Bash job you started): \`-d '{"agent":"${agentName}","label":"server up","until":"curl -sf localhost:3000/health"}'\`.`,
92
97
  `- \`agent\` must be your own name${nameHint}.`,
93
- '- Watches time out (default 30 min). One clear task per watch; do not watch trivial commands you could just run.',
98
+ '- Watches time out (default 30 min, up to 24h pass `timeoutMs` for long trainings).',
94
99
  '- Do not poll or sleep-loop waiting yourself — set the watch and end the turn. That is the whole point.',
95
100
  ];
96
101
  }
102
+
103
+ // Log-path convention. The GUI turns log paths in a message into a one-click
104
+ // "tail" button; a bare or relative filename can't be located reliably (there
105
+ // may be several with the same name in different directories), so agents must
106
+ // give the absolute path.
107
+ export function logFileInstructions() {
108
+ return [
109
+ '## Mentioning Log Files',
110
+ '',
111
+ 'When you start a process that writes to a log the user may want to watch, state the',
112
+ "log's ABSOLUTE path — e.g. `Logging to /home/you/project/runs/train.log` — not a bare",
113
+ 'or relative filename. The GUI turns that path into a one-click "tail" button for the',
114
+ 'user; a bare name like `train.log` cannot be located reliably (several files may share',
115
+ 'it), so always resolve it to a full path first (`realpath` / `readlink -f` if unsure).',
116
+ ];
117
+ }
@@ -4,7 +4,7 @@
4
4
  import { writeFileSync, readFileSync, existsSync, readdirSync, statSync } from 'fs';
5
5
  import { resolve, dirname, basename } from 'path';
6
6
  import { escapeMd } from './validate.js';
7
- import { innerChatInstructions, watchInstructions } from './innerchat-docs.js';
7
+ import { innerChatInstructions, watchInstructions, logFileInstructions } from './innerchat-docs.js';
8
8
 
9
9
  const GROOVE_SECTION_START = '<!-- GROOVE:START -->';
10
10
  const GROOVE_SECTION_END = '<!-- GROOVE:END -->';
@@ -561,7 +561,7 @@ export class Introducer {
561
561
  // compaction — the spawn prompt alone can scroll out of a long session.
562
562
  _innerChatSection() {
563
563
  const port = this.daemon.port || 31415;
564
- return [...innerChatInstructions(port), '', ...watchInstructions(port)];
564
+ return [...innerChatInstructions(port), '', ...watchInstructions(port), '', ...logFileInstructions()];
565
565
  }
566
566
 
567
567
  writeRegistryFile(projectDir) {
@@ -10,7 +10,7 @@ import { LocalProvider } from './providers/local.js';
10
10
  import { OllamaProvider } from './providers/ollama.js';
11
11
  import { AgentLoop } from './agent-loop.js';
12
12
  import { validateAgentConfig } from './validate.js';
13
- import { innerChatInstructions, watchInstructions } from './innerchat-docs.js';
13
+ import { innerChatInstructions, watchInstructions, logFileInstructions } from './innerchat-docs.js';
14
14
 
15
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
16
  const SLIDES_ENGINE_SRC = resolve(__dirname, '../templates/groove-slides.cjs');
@@ -1174,6 +1174,8 @@ For normal file edits within your scope, proceed without review.
1174
1174
  ...innerChatInstructions(port, agent.name),
1175
1175
  '',
1176
1176
  ...watchInstructions(port, agent.name),
1177
+ '',
1178
+ ...logFileInstructions(),
1177
1179
  ].join('\n') + '\n\n';
1178
1180
  if (spawnConfig.prompt.startsWith('# Handoff Brief')) {
1179
1181
  spawnConfig.prompt += '\n\n' + capabilities.trim();
@@ -2626,6 +2628,9 @@ After fixing all issues, run tests (npm test) and build (npm run build) to verif
2626
2628
  // Let the aborted process finish tearing down before re-spawning; resume()
2627
2629
  // kills the current handle and re-registers under a new agent id.
2628
2630
  setTimeout(() => {
2631
+ // The user may have paused/killed during this 500ms window — stop()/kill()
2632
+ // clear the pending message to cancel exactly this in-flight retry.
2633
+ if (!this._pendingUserMessage.has(agentId)) return;
2629
2634
  this.resume(agentId, pending.message).catch((err) => {
2630
2635
  this.daemon.broadcast({
2631
2636
  type: 'agent:output',
@@ -3058,6 +3063,13 @@ After fixing all issues, run tests (npm test) and build (npm run build) to verif
3058
3063
  const handle = this.handles.get(agentId);
3059
3064
  if (!handle) return;
3060
3065
 
3066
+ // A user pause aborts the current turn, which the CLI reports as a dropped
3067
+ // turn (isError, apiDurationMs === 0). Clear the pending message FIRST so
3068
+ // _retryDroppedTurn no-ops — otherwise the stop is auto-retried and the
3069
+ // agent resumes, forcing the user to click Pause twice.
3070
+ this._pendingUserMessage.delete(agentId);
3071
+ this._retryAttemptCarry.delete(agentId);
3072
+
3061
3073
  const { proc, loop } = handle;
3062
3074
 
3063
3075
  if (loop) {
@@ -230,6 +230,21 @@ export class Teams {
230
230
  originalWorkingDir: team.workingDir,
231
231
  };
232
232
  writeFileSync(resolve(archivePath, 'metadata.json'), JSON.stringify(metadata, null, 2));
233
+ } else if (
234
+ team.workingDir &&
235
+ team.workingDir !== this.daemon.projectDir &&
236
+ existsSync(team.workingDir) &&
237
+ this._dirHasExternalOccupants(team.workingDir)
238
+ ) {
239
+ // A moved agent still lives here — archive metadata only, leave the
240
+ // directory in place so that agent keeps working.
241
+ console.log(`[Groove:Teams] Archiving ${team.name} metadata only: agents from other teams still use its directory`);
242
+ mkdirSync(archivePath, { recursive: true });
243
+ writeFileSync(resolve(archivePath, 'metadata.json'), JSON.stringify({
244
+ originalName: team.name, originalId: team.id, mode: team.mode || 'sandbox',
245
+ deletedAt: new Date().toISOString(), agentCount: agents.length,
246
+ originalWorkingDir: team.workingDir, directoryRetained: true,
247
+ }, null, 2));
233
248
  } else if (
234
249
  team.workingDir &&
235
250
  team.workingDir !== this.daemon.projectDir &&
@@ -281,10 +296,16 @@ export class Teams {
281
296
  team.workingDir !== this.daemon.projectDir &&
282
297
  existsSync(team.workingDir)
283
298
  ) {
284
- try {
285
- rmSync(team.workingDir, { recursive: true, force: true });
286
- } catch (err) {
287
- console.log(`[Groove:Teams] Failed to delete directory: ${err.message}`);
299
+ if (this._dirHasExternalOccupants(team.workingDir)) {
300
+ // An agent moved to another team still lives here keep the directory
301
+ // so it doesn't lose its working dir. The team record is still removed.
302
+ console.log(`[Groove:Teams] Keeping ${team.workingDir}: agents from other teams still use it`);
303
+ } else {
304
+ try {
305
+ rmSync(team.workingDir, { recursive: true, force: true });
306
+ } catch (err) {
307
+ console.log(`[Groove:Teams] Failed to delete directory: ${err.message}`);
308
+ }
288
309
  }
289
310
  }
290
311
 
@@ -292,6 +313,17 @@ export class Teams {
292
313
  return true;
293
314
  }
294
315
 
316
+ // True if an agent OUTSIDE this team still lives in the given directory —
317
+ // i.e. an agent that was moved to another team but kept its working dir here.
318
+ // Destroying the directory would break it, so callers skip the removal.
319
+ // Call after _killAndRemoveAgents so this team's own agents are already gone.
320
+ _dirHasExternalOccupants(dir) {
321
+ if (!dir) return false;
322
+ const prefix = `${dir}/`;
323
+ return this.daemon.registry.getAll().some((a) =>
324
+ a.workingDir && (a.workingDir === dir || a.workingDir.startsWith(prefix)));
325
+ }
326
+
295
327
  _killAndRemoveAgents(teamId) {
296
328
  const agents = this.daemon.registry.getAll().filter((a) => a.teamId === teamId);
297
329
  for (const agent of agents) {