groove-dev 0.27.175 → 0.27.180

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 (71) hide show
  1. package/AUTONOMOUS_AGENT_FRAMEWORK.md +654 -0
  2. package/innerchat/Screenshot_2026-06-09_at_2.19.42_PM.png +0 -0
  3. package/moe-training/client/envelope-builder.js +22 -5
  4. package/moe-training/shared/constants.js +11 -1
  5. package/moe-training/shared/envelope-schema.js +11 -3
  6. package/moe-training/test/client/envelope-builder.test.js +42 -6
  7. package/moe-training/test/shared/envelope-schema.test.js +11 -4
  8. package/node_modules/@groove-dev/cli/package.json +1 -1
  9. package/node_modules/@groove-dev/daemon/package.json +1 -1
  10. package/node_modules/@groove-dev/daemon/src/api.js +3 -1
  11. package/node_modules/@groove-dev/daemon/src/autostate.js +198 -0
  12. package/node_modules/@groove-dev/daemon/src/index.js +6 -0
  13. package/node_modules/@groove-dev/daemon/src/orchestrator.js +585 -0
  14. package/node_modules/@groove-dev/daemon/src/process.js +27 -2
  15. package/node_modules/@groove-dev/daemon/src/registry.js +2 -1
  16. package/node_modules/@groove-dev/daemon/src/routes/auto-agents.js +264 -0
  17. package/node_modules/@groove-dev/daemon/src/validate.js +6 -0
  18. package/node_modules/@groove-dev/gui/dist/assets/index-CTer01Vg.js +1065 -0
  19. package/node_modules/@groove-dev/gui/dist/assets/index-DTFtRtkx.css +1 -0
  20. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  21. package/node_modules/@groove-dev/gui/package.json +1 -1
  22. package/node_modules/@groove-dev/gui/src/App.jsx +2 -0
  23. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +53 -0
  24. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +95 -2
  25. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +50 -1
  26. package/node_modules/@groove-dev/gui/src/components/auto-agents/auto-agent-card.jsx +139 -0
  27. package/node_modules/@groove-dev/gui/src/components/auto-agents/auto-agent-detail.jsx +286 -0
  28. package/node_modules/@groove-dev/gui/src/components/auto-agents/setup-wizard.jsx +284 -0
  29. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
  30. package/node_modules/@groove-dev/gui/src/stores/groove.js +14 -0
  31. package/node_modules/@groove-dev/gui/src/stores/slices/auto-agents-slice.js +149 -0
  32. package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +14 -2
  33. package/node_modules/@groove-dev/gui/src/views/auto-agents.jsx +70 -0
  34. package/node_modules/@groove-dev/gui/src/views/settings.jsx +13 -6
  35. package/node_modules/moe-training/client/envelope-builder.js +22 -5
  36. package/node_modules/moe-training/shared/constants.js +11 -1
  37. package/node_modules/moe-training/shared/envelope-schema.js +11 -3
  38. package/node_modules/moe-training/test/client/envelope-builder.test.js +42 -6
  39. package/node_modules/moe-training/test/shared/envelope-schema.test.js +11 -4
  40. package/package.json +1 -1
  41. package/packages/cli/package.json +1 -1
  42. package/packages/daemon/package.json +1 -1
  43. package/packages/daemon/src/api.js +3 -1
  44. package/packages/daemon/src/autostate.js +198 -0
  45. package/packages/daemon/src/index.js +6 -0
  46. package/packages/daemon/src/orchestrator.js +585 -0
  47. package/packages/daemon/src/process.js +27 -2
  48. package/packages/daemon/src/registry.js +2 -1
  49. package/packages/daemon/src/routes/auto-agents.js +264 -0
  50. package/packages/daemon/src/validate.js +6 -0
  51. package/packages/gui/dist/assets/index-CTer01Vg.js +1065 -0
  52. package/packages/gui/dist/assets/index-DTFtRtkx.css +1 -0
  53. package/packages/gui/dist/index.html +2 -2
  54. package/packages/gui/package.json +1 -1
  55. package/packages/gui/src/App.jsx +2 -0
  56. package/packages/gui/src/components/agents/agent-config.jsx +53 -0
  57. package/packages/gui/src/components/agents/agent-panel.jsx +95 -2
  58. package/packages/gui/src/components/agents/spawn-wizard.jsx +50 -1
  59. package/packages/gui/src/components/auto-agents/auto-agent-card.jsx +139 -0
  60. package/packages/gui/src/components/auto-agents/auto-agent-detail.jsx +286 -0
  61. package/packages/gui/src/components/auto-agents/setup-wizard.jsx +284 -0
  62. package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
  63. package/packages/gui/src/stores/groove.js +14 -0
  64. package/packages/gui/src/stores/slices/auto-agents-slice.js +149 -0
  65. package/packages/gui/src/stores/slices/ui-slice.js +14 -2
  66. package/packages/gui/src/views/auto-agents.jsx +70 -0
  67. package/packages/gui/src/views/settings.jsx +13 -6
  68. package/node_modules/@groove-dev/gui/dist/assets/index-BMaxZVeX.js +0 -1035
  69. package/node_modules/@groove-dev/gui/dist/assets/index-BvJwMNAX.css +0 -1
  70. package/packages/gui/dist/assets/index-BMaxZVeX.js +0 -1035
  71. package/packages/gui/dist/assets/index-BvJwMNAX.css +0 -1
@@ -0,0 +1,585 @@
1
+ // GROOVE — Auto Agent Orchestrator
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+ //
4
+ // Manages autonomous agent definitions and their lifecycle:
5
+ // - Create/update/delete auto agent definitions
6
+ // - Cron-driven heartbeat: check status, detect transitions, spawn next iteration
7
+ // - Stale process detection
8
+ // - Goal evaluation and phase transitions
9
+ // - File-based state survives daemon restarts
10
+ //
11
+ // An auto agent is a DEFINITION that spawns real Groove agents on a schedule.
12
+ // Each iteration is a standard spawned agent with full telemetry, chat, and tracking.
13
+
14
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync, copyFileSync, renameSync } from 'fs';
15
+ import { resolve } from 'path';
16
+ import { randomUUID } from 'crypto';
17
+
18
+ const CHECK_INTERVAL = 60_000; // 1 minute
19
+ const DEFAULT_TIMEOUT = 600; // 10 minutes per handoff
20
+ const DEFAULT_STALE_MINUTES = 240; // 4 hours
21
+ const MAX_HISTORY = 50;
22
+
23
+ function parseCronField(field, min, max) {
24
+ if (field === '*') return null;
25
+ if (field.startsWith('*/')) {
26
+ const step = parseInt(field.slice(2), 10);
27
+ if (isNaN(step) || step <= 0) return null;
28
+ return { type: 'step', step };
29
+ }
30
+ if (field.includes(',')) {
31
+ const values = field.split(',').map(v => parseInt(v.trim(), 10)).filter(v => !isNaN(v) && v >= min && v <= max);
32
+ if (values.length === 0) return null;
33
+ return { type: 'list', values };
34
+ }
35
+ if (field.includes('-')) {
36
+ const [s, e] = field.split('-');
37
+ const start = parseInt(s, 10), end = parseInt(e, 10);
38
+ if (isNaN(start) || isNaN(end) || start < min || end > max) return null;
39
+ return { type: 'range', start, end };
40
+ }
41
+ const val = parseInt(field, 10);
42
+ if (!isNaN(val) && val >= min && val <= max) return { type: 'exact', value: val };
43
+ return null;
44
+ }
45
+
46
+ function fieldMatches(parsed, value) {
47
+ if (parsed === null) return true;
48
+ if (parsed.type === 'exact') return value === parsed.value;
49
+ if (parsed.type === 'step') return value % parsed.step === 0;
50
+ if (parsed.type === 'list') return parsed.values.includes(value);
51
+ if (parsed.type === 'range') return value >= parsed.start && value <= parsed.end;
52
+ return true;
53
+ }
54
+
55
+ function cronMatches(cronExpr, date) {
56
+ const parts = cronExpr.trim().split(/\s+/);
57
+ if (parts.length !== 5) return false;
58
+ const fields = [
59
+ parseCronField(parts[0], 0, 59),
60
+ parseCronField(parts[1], 0, 23),
61
+ parseCronField(parts[2], 1, 31),
62
+ parseCronField(parts[3], 1, 12),
63
+ parseCronField(parts[4], 0, 6),
64
+ ];
65
+ return (
66
+ fieldMatches(fields[0], date.getMinutes()) &&
67
+ fieldMatches(fields[1], date.getHours()) &&
68
+ fieldMatches(fields[2], date.getDate()) &&
69
+ fieldMatches(fields[3], date.getMonth() + 1) &&
70
+ fieldMatches(fields[4], date.getDay())
71
+ );
72
+ }
73
+
74
+ function describeCron(cron) {
75
+ const presets = {
76
+ '*/15 * * * *': 'Every 15 minutes',
77
+ '*/30 * * * *': 'Every 30 minutes',
78
+ '0 * * * *': 'Every hour',
79
+ '0 */2 * * *': 'Every 2 hours',
80
+ '0 */6 * * *': 'Every 6 hours',
81
+ '0 0 * * *': 'Daily at midnight',
82
+ '0 9 * * *': 'Daily at 9:00 AM',
83
+ '0 9 * * 1-5': 'Weekdays at 9:00 AM',
84
+ };
85
+ return presets[cron] || cron;
86
+ }
87
+
88
+ export class Orchestrator {
89
+ constructor(daemon) {
90
+ this.daemon = daemon;
91
+ this.defsDir = resolve(daemon.grooveDir, 'auto-agents');
92
+ mkdirSync(this.defsDir, { recursive: true });
93
+ this.definitions = new Map();
94
+ this.activeAgents = new Map(); // defId -> agentId
95
+ this.interval = null;
96
+ this._load();
97
+ }
98
+
99
+ // --- CRUD ---
100
+
101
+ create(config) {
102
+ if (!config.name) throw new Error('Name is required');
103
+ if (!config.cadence) throw new Error('Cadence (cron expression) is required');
104
+ const parts = config.cadence.trim().split(/\s+/);
105
+ if (parts.length !== 5) throw new Error('Invalid cron expression (must be 5 fields)');
106
+
107
+ const id = randomUUID().slice(0, 8);
108
+ const def = {
109
+ id,
110
+ name: config.name,
111
+ description: config.description || '',
112
+ cadence: config.cadence.trim(),
113
+ cadenceDescription: describeCron(config.cadence.trim()),
114
+ timeout: config.timeout || DEFAULT_TIMEOUT,
115
+ staleThresholdMinutes: config.staleThresholdMinutes || DEFAULT_STALE_MINUTES,
116
+ enabled: config.enabled !== false,
117
+ paused: false,
118
+
119
+ // Agent spawn config
120
+ agentConfig: {
121
+ role: config.role || 'fullstack',
122
+ provider: config.provider || this.daemon.config?.defaultProvider || 'claude-code',
123
+ model: config.model || 'auto',
124
+ permission: config.permission || 'auto',
125
+ scope: config.scope || [],
126
+ workingDir: config.workingDir || this.daemon.config?.defaultWorkingDir || this.daemon.projectDir,
127
+ skills: config.skills || [],
128
+ integrations: config.integrations || [],
129
+ },
130
+
131
+ // Notifications
132
+ notifications: config.notifications || [],
133
+
134
+ // Guardrails
135
+ guardrails: config.guardrails || [],
136
+ maxIterations: config.maxIterations || null, // null = infinite
137
+ maxCostPerCycle: config.maxCostPerCycle || null,
138
+
139
+ // Tracking
140
+ totalCycles: 0,
141
+ totalCost: 0,
142
+ consecutiveFailures: 0,
143
+ lastRunAt: null,
144
+ lastRunStatus: null,
145
+ lastAgentId: null,
146
+ createdAt: new Date().toISOString(),
147
+ updatedAt: new Date().toISOString(),
148
+ };
149
+
150
+ this.definitions.set(id, def);
151
+ this._save(id);
152
+
153
+ // Initialize state layer files
154
+ const autoState = this.daemon.autoState;
155
+ autoState.getState(id); // creates default state
156
+ if (config.prompt) autoState.setPrompt(id, config.prompt);
157
+ if (config.roadmap) autoState.setRoadmap(id, config.roadmap);
158
+
159
+ this.daemon.broadcast({ type: 'auto-agent:created', data: def });
160
+ this.daemon.audit.log('autoAgent.create', { id, name: def.name });
161
+
162
+ return def;
163
+ }
164
+
165
+ get(id) {
166
+ return this.definitions.get(id) || null;
167
+ }
168
+
169
+ list() {
170
+ return [...this.definitions.values()];
171
+ }
172
+
173
+ update(id, updates) {
174
+ const def = this.definitions.get(id);
175
+ if (!def) throw new Error(`Auto agent not found: ${id}`);
176
+
177
+ const allowed = [
178
+ 'name', 'description', 'cadence', 'timeout', 'staleThresholdMinutes',
179
+ 'enabled', 'notifications', 'guardrails', 'maxIterations', 'maxCostPerCycle',
180
+ ];
181
+ for (const key of allowed) {
182
+ if (key in updates) def[key] = updates[key];
183
+ }
184
+
185
+ if (updates.cadence) {
186
+ const parts = updates.cadence.trim().split(/\s+/);
187
+ if (parts.length !== 5) throw new Error('Invalid cron expression');
188
+ def.cadence = updates.cadence.trim();
189
+ def.cadenceDescription = describeCron(def.cadence);
190
+ }
191
+
192
+ if (updates.agentConfig) {
193
+ def.agentConfig = { ...def.agentConfig, ...updates.agentConfig };
194
+ }
195
+
196
+ def.updatedAt = new Date().toISOString();
197
+ this._save(id);
198
+
199
+ this.daemon.broadcast({ type: 'auto-agent:updated', data: def });
200
+ return def;
201
+ }
202
+
203
+ delete(id) {
204
+ const def = this.definitions.get(id);
205
+ if (!def) throw new Error(`Auto agent not found: ${id}`);
206
+
207
+ // Kill active agent if running
208
+ const activeId = this.activeAgents.get(id);
209
+ if (activeId) {
210
+ try { this.daemon.processes.kill(activeId); } catch { /* already dead */ }
211
+ this.activeAgents.delete(id);
212
+ }
213
+
214
+ this.definitions.delete(id);
215
+ const filePath = resolve(this.defsDir, `${id}.json`);
216
+ try { unlinkSync(filePath); } catch { /* already gone */ }
217
+
218
+ this.daemon.broadcast({ type: 'auto-agent:deleted', data: { id } });
219
+ this.daemon.audit.log('autoAgent.delete', { id, name: def.name });
220
+ return def;
221
+ }
222
+
223
+ // --- Controls ---
224
+
225
+ pause(id) {
226
+ const def = this.definitions.get(id);
227
+ if (!def) throw new Error(`Auto agent not found: ${id}`);
228
+ def.paused = true;
229
+ def.updatedAt = new Date().toISOString();
230
+ this._save(id);
231
+ this.daemon.autoState.setState(id, { paused: true });
232
+ this.daemon.broadcast({ type: 'auto-agent:paused', data: { id } });
233
+ this.daemon.audit.log('autoAgent.pause', { id, name: def.name });
234
+ return def;
235
+ }
236
+
237
+ resume(id) {
238
+ const def = this.definitions.get(id);
239
+ if (!def) throw new Error(`Auto agent not found: ${id}`);
240
+ def.paused = false;
241
+ def.consecutiveFailures = 0;
242
+ def.updatedAt = new Date().toISOString();
243
+ this._save(id);
244
+ this.daemon.autoState.setState(id, { paused: false, error: null });
245
+ this.daemon.broadcast({ type: 'auto-agent:resumed', data: { id } });
246
+ this.daemon.audit.log('autoAgent.resume', { id, name: def.name });
247
+ return def;
248
+ }
249
+
250
+ async trigger(id) {
251
+ const def = this.definitions.get(id);
252
+ if (!def) throw new Error(`Auto agent not found: ${id}`);
253
+ return this._executeIteration(def, 'manual');
254
+ }
255
+
256
+ // --- Heartbeat ---
257
+
258
+ start() {
259
+ if (this.interval) return;
260
+ this.interval = setInterval(() => this._check(), CHECK_INTERVAL);
261
+ }
262
+
263
+ stop() {
264
+ if (this.interval) {
265
+ clearInterval(this.interval);
266
+ this.interval = null;
267
+ }
268
+ }
269
+
270
+ _check() {
271
+ const now = new Date();
272
+ for (const def of this.definitions.values()) {
273
+ if (!def.enabled || def.paused) continue;
274
+
275
+ // Max iterations check
276
+ if (def.maxIterations && def.totalCycles >= def.maxIterations) continue;
277
+
278
+ // Auto-pause after 5 consecutive failures
279
+ if (def.consecutiveFailures >= 5) {
280
+ if (!def.paused) {
281
+ def.paused = true;
282
+ this._save(def.id);
283
+ this.daemon.autoState.setState(def.id, {
284
+ paused: true,
285
+ error: `Auto-paused after ${def.consecutiveFailures} consecutive failures`,
286
+ });
287
+ this.daemon.broadcast({ type: 'auto-agent:auto-paused', data: { id: def.id, reason: 'consecutive_failures' } });
288
+ }
289
+ continue;
290
+ }
291
+
292
+ // Check if an agent is currently running
293
+ const activeId = this.activeAgents.get(def.id);
294
+ if (activeId) {
295
+ const agent = this.daemon.registry.get(activeId);
296
+ if (agent && (agent.status === 'running' || agent.status === 'starting')) {
297
+ this._checkStale(def, agent);
298
+ continue;
299
+ }
300
+ // Agent finished — handle transition
301
+ this.activeAgents.delete(def.id);
302
+ this._handleCompletion(def, agent);
303
+ continue;
304
+ }
305
+
306
+ // No agent running — check if cron matches
307
+ if (cronMatches(def.cadence, now)) {
308
+ this._executeIteration(def, 'cron').catch(err => {
309
+ console.log(`[Groove:Orchestrator] Failed to execute ${def.name}: ${err.message}`);
310
+ });
311
+ }
312
+ }
313
+ }
314
+
315
+ _checkStale(def, agent) {
316
+ if (!agent.lastActivity) return;
317
+ const staleMs = def.staleThresholdMinutes * 60 * 1000;
318
+ const elapsed = Date.now() - new Date(agent.lastActivity).getTime();
319
+ if (elapsed > staleMs) {
320
+ console.log(`[Groove:Orchestrator] Agent ${agent.id} for ${def.name} is stale (${Math.round(elapsed / 60000)}min idle)`);
321
+ this.daemon.autoState.appendJournal(def.id, {
322
+ event: 'stale_detected',
323
+ details: `Agent ${agent.id} idle for ${Math.round(elapsed / 60000)} minutes — killing`,
324
+ agentId: agent.id,
325
+ });
326
+ try { this.daemon.processes.kill(agent.id); } catch { /* already dead */ }
327
+ this.activeAgents.delete(def.id);
328
+ }
329
+ }
330
+
331
+ _handleCompletion(def, agent) {
332
+ const status = agent?.status || 'unknown';
333
+ const runId = randomUUID().slice(0, 8);
334
+
335
+ // Log the run
336
+ this.daemon.autoState.logRun(def.id, runId, {
337
+ agentId: agent?.id,
338
+ status,
339
+ startedAt: agent?.spawnedAt,
340
+ completedAt: new Date().toISOString(),
341
+ tokensUsed: agent?.tokensUsed || 0,
342
+ costUsd: agent?.costUsd || 0,
343
+ turns: agent?.turns || 0,
344
+ });
345
+
346
+ // Update def tracking
347
+ def.totalCycles++;
348
+ def.totalCost += (agent?.costUsd || 0);
349
+ def.lastRunStatus = status;
350
+ def.lastAgentId = agent?.id;
351
+ def.updatedAt = new Date().toISOString();
352
+
353
+ if (status === 'completed') {
354
+ def.consecutiveFailures = 0;
355
+ this.daemon.autoState.appendJournal(def.id, {
356
+ event: 'cycle_completed',
357
+ cycle: def.totalCycles,
358
+ details: `Cycle ${def.totalCycles} completed successfully`,
359
+ agentId: agent?.id,
360
+ cost: agent?.costUsd || 0,
361
+ });
362
+ } else {
363
+ def.consecutiveFailures++;
364
+ this.daemon.autoState.appendJournal(def.id, {
365
+ event: 'cycle_failed',
366
+ cycle: def.totalCycles,
367
+ details: `Cycle ${def.totalCycles} ended with status: ${status}`,
368
+ agentId: agent?.id,
369
+ error: status,
370
+ });
371
+ }
372
+
373
+ // Update state
374
+ const state = this.daemon.autoState.getState(def.id);
375
+ this.daemon.autoState.setState(def.id, {
376
+ cycle: def.totalCycles,
377
+ current_run: null,
378
+ });
379
+
380
+ this._save(def.id);
381
+ this.daemon.broadcast({ type: 'auto-agent:cycle-complete', data: { id: def.id, cycle: def.totalCycles, status } });
382
+ }
383
+
384
+ async _executeIteration(def, trigger) {
385
+ const autoState = this.daemon.autoState;
386
+ const state = autoState.getState(def.id);
387
+ const journal = autoState.getJournal(def.id, { limit: 20 });
388
+ const roadmap = autoState.getRoadmap(def.id);
389
+ const prompt = autoState.getPrompt(def.id);
390
+
391
+ // Build the full context prompt
392
+ const contextPrompt = this._buildPrompt(def, state, journal, roadmap, prompt, trigger);
393
+
394
+ // Spawn a real Groove agent
395
+ const agentConfig = {
396
+ ...def.agentConfig,
397
+ prompt: contextPrompt,
398
+ name: `auto-${def.name.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 20)}-c${(def.totalCycles + 1)}`,
399
+ metadata: {
400
+ autoAgentId: def.id,
401
+ autoAgentName: def.name,
402
+ cycle: def.totalCycles + 1,
403
+ trigger,
404
+ isAutoAgent: true,
405
+ },
406
+ };
407
+
408
+ const agent = await this.daemon.processes.spawn(agentConfig);
409
+
410
+ this.activeAgents.set(def.id, agent.id);
411
+ def.lastRunAt = new Date().toISOString();
412
+ def.lastRunStatus = 'running';
413
+ def.lastAgentId = agent.id;
414
+ this._save(def.id);
415
+
416
+ autoState.setState(def.id, {
417
+ current_run: {
418
+ agentId: agent.id,
419
+ cycle: def.totalCycles + 1,
420
+ trigger,
421
+ startedAt: new Date().toISOString(),
422
+ },
423
+ });
424
+
425
+ autoState.appendJournal(def.id, {
426
+ event: 'cycle_started',
427
+ cycle: def.totalCycles + 1,
428
+ trigger,
429
+ agentId: agent.id,
430
+ });
431
+
432
+ this.daemon.broadcast({
433
+ type: 'auto-agent:iteration-started',
434
+ data: { id: def.id, agentId: agent.id, cycle: def.totalCycles + 1, trigger },
435
+ });
436
+
437
+ this.daemon.audit.log('autoAgent.iterate', {
438
+ id: def.id, name: def.name, agentId: agent.id,
439
+ cycle: def.totalCycles + 1, trigger,
440
+ });
441
+
442
+ // Watch for completion
443
+ this._watchAgent(def.id, agent.id);
444
+
445
+ return agent;
446
+ }
447
+
448
+ _buildPrompt(def, state, journal, roadmap, systemPrompt, trigger) {
449
+ const parts = [];
450
+
451
+ // System prompt (identity, tools, rules, philosophy)
452
+ if (systemPrompt) {
453
+ parts.push(systemPrompt);
454
+ } else {
455
+ parts.push(`# Auto Agent: ${def.name}\n\n${def.description || 'Autonomous agent.'}`);
456
+ parts.push(`\nYou are an autonomous agent. You own this task — analyze, decide, execute.`);
457
+ parts.push(`Update your state and journal with your decisions and findings.`);
458
+ parts.push(`\n## How to Think About Failure\nA regression is not a reason to stop — it's a data point. Extract the lesson and design a better experiment. Never park yourself waiting for approval — keep iterating.`);
459
+ }
460
+
461
+ // Current situation
462
+ parts.push(`\n---\n## Current Situation`);
463
+ parts.push(`**Trigger:** ${trigger}`);
464
+ parts.push(`**Phase:** ${state.phase || 'idle'}`);
465
+ if (state.phase_note) parts.push(`**Phase Note:** ${state.phase_note}`);
466
+ parts.push(`**Cycle:** ${state.cycle + 1}`);
467
+
468
+ if (state.champion) {
469
+ parts.push(`\n**Current Champion:**`);
470
+ parts.push('```json\n' + JSON.stringify(state.champion, null, 2) + '\n```');
471
+ }
472
+
473
+ // History (last 10)
474
+ if (Array.isArray(state.history) && state.history.length > 0) {
475
+ parts.push(`\n## Recent History (last ${Math.min(state.history.length, 10)})`);
476
+ for (const h of state.history.slice(-10)) {
477
+ parts.push(`- [${h.timestamp || '?'}] ${h.tag || h.event || 'entry'}: ${h.note || h.details || JSON.stringify(h)}`);
478
+ }
479
+ }
480
+
481
+ // Journal (accumulated knowledge)
482
+ if (journal.length > 0) {
483
+ parts.push(`\n## Journal (accumulated knowledge from previous cycles)`);
484
+ for (const j of journal) {
485
+ parts.push(`- [${j.timestamp}] **${j.event}**: ${j.details || ''}`);
486
+ if (j.lesson) parts.push(` *Lesson:* ${j.lesson}`);
487
+ }
488
+ }
489
+
490
+ // Roadmap
491
+ if (roadmap) {
492
+ parts.push(`\n## Roadmap\n${roadmap}`);
493
+ }
494
+
495
+ // Guardrails
496
+ if (def.guardrails && def.guardrails.length > 0) {
497
+ parts.push(`\n## Guardrails (MUST NOT violate)`);
498
+ for (const g of def.guardrails) {
499
+ parts.push(`- ${typeof g === 'string' ? g : g.description || JSON.stringify(g)}`);
500
+ }
501
+ }
502
+
503
+ // Instructions for state management
504
+ parts.push(`\n---\n## State Management`);
505
+ parts.push(`When you complete work, update your findings by writing to the state files:`);
506
+ parts.push(`- Update phase/phase_note via the GROOVE API: \`POST http://localhost:31415/api/auto-agents/${def.id}/state\``);
507
+ parts.push(`- Add journal entries: \`POST http://localhost:31415/api/auto-agents/${def.id}/journal\``);
508
+ parts.push(`- Add history entries: \`POST http://localhost:31415/api/auto-agents/${def.id}/history\``);
509
+ parts.push(`\nTake ownership. Analyze, decide, and execute. Keep moving forward.`);
510
+
511
+ return parts.join('\n');
512
+ }
513
+
514
+ _watchAgent(defId, agentId) {
515
+ const checkInterval = setInterval(() => {
516
+ const agent = this.daemon.registry.get(agentId);
517
+ if (!agent) {
518
+ clearInterval(checkInterval);
519
+ this.activeAgents.delete(defId);
520
+ return;
521
+ }
522
+ const terminal = new Set(['completed', 'crashed', 'stopped', 'killed']);
523
+ if (!terminal.has(agent.status)) return;
524
+ clearInterval(checkInterval);
525
+
526
+ // Let the next _check() cycle handle the transition
527
+ // (activeAgents still has the mapping, _check will see the terminal status)
528
+ }, 5000);
529
+ }
530
+
531
+ // --- Persistence ---
532
+
533
+ _load() {
534
+ if (!existsSync(this.defsDir)) return;
535
+ for (const file of readdirSync(this.defsDir)) {
536
+ if (!file.endsWith('.json')) continue;
537
+ try {
538
+ const data = JSON.parse(readFileSync(resolve(this.defsDir, file), 'utf8'));
539
+ if (data.id) {
540
+ // Reset running state on load (daemon restarted)
541
+ data.lastRunStatus = data.lastRunStatus === 'running' ? 'interrupted' : data.lastRunStatus;
542
+ this.definitions.set(data.id, data);
543
+ }
544
+ } catch (err) {
545
+ console.log(`[Groove:Orchestrator] Failed to load ${file}: ${err.message}`);
546
+ }
547
+ }
548
+ }
549
+
550
+ _save(id) {
551
+ const def = this.definitions.get(id);
552
+ if (!def) return;
553
+ const filePath = resolve(this.defsDir, `${id}.json`);
554
+ writeFileSync(filePath, JSON.stringify(def, null, 2));
555
+ }
556
+
557
+ // --- Status ---
558
+
559
+ getStatus(id) {
560
+ const def = this.definitions.get(id);
561
+ if (!def) return null;
562
+ const state = this.daemon.autoState.getState(id);
563
+ const activeAgentId = this.activeAgents.get(id);
564
+ let activeAgent = null;
565
+ if (activeAgentId) {
566
+ const agent = this.daemon.registry.get(activeAgentId);
567
+ if (agent) {
568
+ activeAgent = {
569
+ id: agent.id,
570
+ status: agent.status,
571
+ tokensUsed: agent.tokensUsed,
572
+ costUsd: agent.costUsd,
573
+ turns: agent.turns,
574
+ spawnedAt: agent.spawnedAt,
575
+ lastActivity: agent.lastActivity,
576
+ };
577
+ }
578
+ }
579
+ return {
580
+ definition: def,
581
+ state,
582
+ activeAgent,
583
+ };
584
+ }
585
+ }
@@ -324,6 +324,27 @@ function sanitizeFilename(name) {
324
324
  return String(name).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
325
325
  }
326
326
 
327
+ // Apply Claude Code billing mode to a spawn env, in place.
328
+ // The `claude` CLI bills to usage credits when ANTHROPIC_API_KEY is present and
329
+ // to the OAuth subscription otherwise. 'subscription' (default) strips any
330
+ // inherited key so login auth is used; 'usage' injects the stored claude-code key.
331
+ export function applyClaudeAuthMode(spawnEnv, agent, config, credentials, logStream) {
332
+ if ((agent?.provider || config?.provider) !== 'claude-code') return;
333
+ const mode = agent?.authMode || config?.authMode || 'subscription';
334
+ const log = (msg) => { try { logStream?.write?.(`[${new Date().toISOString()}] ${msg}\n`); } catch { /* noop */ } };
335
+ if (mode === 'usage') {
336
+ const usageKey = credentials?.getKey('claude-code');
337
+ if (usageKey) {
338
+ spawnEnv.ANTHROPIC_API_KEY = usageKey;
339
+ return;
340
+ }
341
+ log('WARN: authMode=usage but no claude-code API key stored — falling back to subscription auth');
342
+ }
343
+ // Subscription mode (or usage fallback): ensure no key leaks in from the daemon's env
344
+ delete spawnEnv.ANTHROPIC_API_KEY;
345
+ delete spawnEnv.ANTHROPIC_AUTH_TOKEN;
346
+ }
347
+
327
348
  export function wrapWithRoleReminder(role, message) {
328
349
  if (role === 'planner' && !message.startsWith('ROLE REMINDER:')) {
329
350
  return 'ROLE REMINDER: You are a PLANNING ONLY agent. Do NOT write code, edit source files, or run build commands. Your ONLY file write should be .groove/recommended-team.json.\n\nUser message: ' + message;
@@ -1299,9 +1320,11 @@ For normal file edits within your scope, proceed without review.
1299
1320
 
1300
1321
  // Spawn the process (use pipe for stdin if provider needs to send prompt via stdin)
1301
1322
  const spawnCwd = [providerCwd, agent.workingDir, this.daemon.projectDir].find(d => d && existsSync(d)) || this.daemon.projectDir;
1323
+ const spawnEnv = { ...process.env, ...env, ...integrationEnv, GROOVE_AGENT_ID: agent.id, GROOVE_AGENT_NAME: agent.name, GROOVE_DAEMON_HOST: this.daemon.host || '127.0.0.1', GROOVE_DAEMON_PORT: String(this.daemon.port || 31415) };
1324
+ applyClaudeAuthMode(spawnEnv, agent, config, this.daemon.credentials, logStream);
1302
1325
  const proc = cpSpawn(command, args, {
1303
1326
  cwd: spawnCwd,
1304
- env: { ...process.env, ...env, ...integrationEnv, GROOVE_AGENT_ID: agent.id, GROOVE_AGENT_NAME: agent.name, GROOVE_DAEMON_HOST: this.daemon.host || '127.0.0.1', GROOVE_DAEMON_PORT: String(this.daemon.port || 31415) },
1327
+ env: spawnEnv,
1305
1328
  stdio: [stdinData ? 'pipe' : 'ignore', 'pipe', 'pipe'],
1306
1329
  detached: false,
1307
1330
  });
@@ -2580,9 +2603,11 @@ After fixing all issues, run tests (npm test) and build (npm run build) to verif
2580
2603
 
2581
2604
  // Spawn the resumed process
2582
2605
  const resumeCwd = [config.workingDir, this.daemon.projectDir].find(d => d && existsSync(d)) || this.daemon.projectDir;
2606
+ const resumeEnv = { ...process.env, ...env, GROOVE_AGENT_ID: newAgent.id, GROOVE_AGENT_NAME: newAgent.name, GROOVE_DAEMON_HOST: this.daemon.host || '127.0.0.1', GROOVE_DAEMON_PORT: String(this.daemon.port || 31415) };
2607
+ applyClaudeAuthMode(resumeEnv, newAgent, config, this.daemon.credentials, logStream);
2583
2608
  const proc = cpSpawn(command, args, {
2584
2609
  cwd: resumeCwd,
2585
- env: { ...process.env, ...env, GROOVE_AGENT_ID: newAgent.id, GROOVE_AGENT_NAME: newAgent.name, GROOVE_DAEMON_HOST: this.daemon.host || '127.0.0.1', GROOVE_DAEMON_PORT: String(this.daemon.port || 31415) },
2610
+ env: resumeEnv,
2586
2611
  stdio: ['ignore', 'pipe', 'pipe'],
2587
2612
  detached: false,
2588
2613
  });
@@ -4,7 +4,7 @@
4
4
  import { EventEmitter } from 'events';
5
5
  import { randomUUID } from 'crypto';
6
6
 
7
- const SAFE_FIELDS = new Set(['status', 'pid', 'tokensUsed', 'contextUsage', 'lastActivity', 'model', 'provider', 'name', 'routingMode', 'routingReason', 'sessionId', 'skills', 'integrations', 'repos', 'workingDir', 'effort', 'costUsd', 'durationMs', 'turns', 'inputTokens', 'outputTokens', 'teamId', 'permission', 'scope', 'integrationApproval', 'personality', 'metadata']);
7
+ const SAFE_FIELDS = new Set(['status', 'pid', 'tokensUsed', 'contextUsage', 'lastActivity', 'model', 'provider', 'name', 'routingMode', 'routingReason', 'authMode', 'sessionId', 'skills', 'integrations', 'repos', 'workingDir', 'effort', 'costUsd', 'durationMs', 'turns', 'inputTokens', 'outputTokens', 'teamId', 'permission', 'scope', 'integrationApproval', 'personality', 'metadata']);
8
8
 
9
9
  export class Registry extends EventEmitter {
10
10
  constructor(state) {
@@ -46,6 +46,7 @@ export class Registry extends EventEmitter {
46
46
  scope: config.scope || [],
47
47
  provider: config.provider || 'claude-code',
48
48
  model: config.model || null,
49
+ authMode: config.authMode || 'subscription',
49
50
  prompt: config.prompt || '',
50
51
  permission: config.permission || 'full',
51
52
  workingDir: config.workingDir || process.cwd(),