groove-dev 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +197 -0
- package/LICENSE +40 -0
- package/README.md +115 -0
- package/docs/GUI_DESIGN_SPEC.md +402 -0
- package/favicon.png +0 -0
- package/groove-logo-short.png +0 -0
- package/groove-logo.png +0 -0
- package/package.json +70 -0
- package/packages/cli/bin/groove.js +98 -0
- package/packages/cli/package.json +15 -0
- package/packages/cli/src/client.js +25 -0
- package/packages/cli/src/commands/agents.js +38 -0
- package/packages/cli/src/commands/approve.js +50 -0
- package/packages/cli/src/commands/config.js +35 -0
- package/packages/cli/src/commands/kill.js +15 -0
- package/packages/cli/src/commands/nuke.js +19 -0
- package/packages/cli/src/commands/providers.js +40 -0
- package/packages/cli/src/commands/rotate.js +16 -0
- package/packages/cli/src/commands/spawn.js +91 -0
- package/packages/cli/src/commands/start.js +31 -0
- package/packages/cli/src/commands/status.js +38 -0
- package/packages/cli/src/commands/stop.js +15 -0
- package/packages/cli/src/commands/team.js +77 -0
- package/packages/daemon/package.json +18 -0
- package/packages/daemon/src/adaptive.js +237 -0
- package/packages/daemon/src/api.js +533 -0
- package/packages/daemon/src/classifier.js +126 -0
- package/packages/daemon/src/credentials.js +121 -0
- package/packages/daemon/src/firstrun.js +93 -0
- package/packages/daemon/src/index.js +208 -0
- package/packages/daemon/src/introducer.js +238 -0
- package/packages/daemon/src/journalist.js +600 -0
- package/packages/daemon/src/lockmanager.js +58 -0
- package/packages/daemon/src/pm.js +108 -0
- package/packages/daemon/src/process.js +361 -0
- package/packages/daemon/src/providers/aider.js +72 -0
- package/packages/daemon/src/providers/base.js +38 -0
- package/packages/daemon/src/providers/claude-code.js +167 -0
- package/packages/daemon/src/providers/codex.js +68 -0
- package/packages/daemon/src/providers/gemini.js +62 -0
- package/packages/daemon/src/providers/index.js +38 -0
- package/packages/daemon/src/providers/ollama.js +94 -0
- package/packages/daemon/src/registry.js +89 -0
- package/packages/daemon/src/rotator.js +185 -0
- package/packages/daemon/src/router.js +132 -0
- package/packages/daemon/src/state.js +34 -0
- package/packages/daemon/src/supervisor.js +178 -0
- package/packages/daemon/src/teams.js +203 -0
- package/packages/daemon/src/terminal/base.js +27 -0
- package/packages/daemon/src/terminal/generic.js +27 -0
- package/packages/daemon/src/terminal/tmux.js +64 -0
- package/packages/daemon/src/tokentracker.js +124 -0
- package/packages/daemon/src/validate.js +122 -0
- package/packages/daemon/templates/api-builder.json +18 -0
- package/packages/daemon/templates/fullstack.json +18 -0
- package/packages/daemon/templates/monorepo.json +24 -0
- package/packages/gui/dist/assets/index-BO95Rm1F.js +73 -0
- package/packages/gui/dist/assets/index-CPzm9ZE9.css +1 -0
- package/packages/gui/dist/favicon.png +0 -0
- package/packages/gui/dist/groove-logo-short.png +0 -0
- package/packages/gui/dist/groove-logo.png +0 -0
- package/packages/gui/dist/index.html +13 -0
- package/packages/gui/index.html +12 -0
- package/packages/gui/package.json +22 -0
- package/packages/gui/public/favicon.png +0 -0
- package/packages/gui/public/groove-logo-short.png +0 -0
- package/packages/gui/public/groove-logo.png +0 -0
- package/packages/gui/src/App.jsx +215 -0
- package/packages/gui/src/components/AgentActions.jsx +347 -0
- package/packages/gui/src/components/AgentChat.jsx +479 -0
- package/packages/gui/src/components/AgentNode.jsx +117 -0
- package/packages/gui/src/components/AgentPanel.jsx +115 -0
- package/packages/gui/src/components/AgentStats.jsx +333 -0
- package/packages/gui/src/components/ApprovalQueue.jsx +156 -0
- package/packages/gui/src/components/EmptyState.jsx +100 -0
- package/packages/gui/src/components/SpawnPanel.jsx +515 -0
- package/packages/gui/src/components/TeamSelector.jsx +162 -0
- package/packages/gui/src/main.jsx +9 -0
- package/packages/gui/src/stores/groove.js +247 -0
- package/packages/gui/src/theme.css +67 -0
- package/packages/gui/src/views/AgentTree.jsx +148 -0
- package/packages/gui/src/views/CommandCenter.jsx +620 -0
- package/packages/gui/src/views/JournalistFeed.jsx +149 -0
- package/packages/gui/vite.config.js +19 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// GROOVE — Adaptive Rotation Threshold System
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
5
|
+
import { resolve } from 'path';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_THRESHOLD = 0.75;
|
|
8
|
+
const NUDGE_UP = 0.02; // Good session → allow more context
|
|
9
|
+
const NUDGE_DOWN = 0.05; // Bad session → rotate sooner
|
|
10
|
+
const MIN_THRESHOLD = 0.40;
|
|
11
|
+
const MAX_THRESHOLD = 0.95;
|
|
12
|
+
const CONVERGENCE_WINDOW = 10; // Stable if last N adjustments < 1%
|
|
13
|
+
|
|
14
|
+
export class AdaptiveThresholds {
|
|
15
|
+
constructor(grooveDir) {
|
|
16
|
+
this.path = resolve(grooveDir, 'rotation-profiles.json');
|
|
17
|
+
this.profiles = {}; // key: `${provider}:${role}` -> { threshold, history[], converged }
|
|
18
|
+
this.load();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
load() {
|
|
22
|
+
if (existsSync(this.path)) {
|
|
23
|
+
try {
|
|
24
|
+
this.profiles = JSON.parse(readFileSync(this.path, 'utf8'));
|
|
25
|
+
} catch {
|
|
26
|
+
this.profiles = {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
save() {
|
|
32
|
+
writeFileSync(this.path, JSON.stringify(this.profiles, null, 2));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
profileKey(provider, role) {
|
|
36
|
+
return `${provider}:${role}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getProfile(provider, role) {
|
|
40
|
+
const key = this.profileKey(provider, role);
|
|
41
|
+
if (!this.profiles[key]) {
|
|
42
|
+
this.profiles[key] = {
|
|
43
|
+
threshold: DEFAULT_THRESHOLD,
|
|
44
|
+
history: [],
|
|
45
|
+
converged: false,
|
|
46
|
+
adjustmentCount: 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return this.profiles[key];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getThreshold(provider, role) {
|
|
53
|
+
return this.getProfile(provider, role).threshold;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Score a completed session and adjust the threshold
|
|
57
|
+
recordSession(provider, role, signals) {
|
|
58
|
+
const profile = this.getProfile(provider, role);
|
|
59
|
+
const score = this.scoreSession(signals);
|
|
60
|
+
|
|
61
|
+
const oldThreshold = profile.threshold;
|
|
62
|
+
let newThreshold;
|
|
63
|
+
|
|
64
|
+
if (score >= 70) {
|
|
65
|
+
// Good session — nudge threshold up (allow more context before rotating)
|
|
66
|
+
newThreshold = Math.min(profile.threshold + NUDGE_UP, MAX_THRESHOLD);
|
|
67
|
+
} else if (score < 40) {
|
|
68
|
+
// Bad session — nudge threshold down (rotate sooner)
|
|
69
|
+
newThreshold = Math.max(profile.threshold - NUDGE_DOWN, MIN_THRESHOLD);
|
|
70
|
+
} else {
|
|
71
|
+
// Neutral — no change
|
|
72
|
+
newThreshold = profile.threshold;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
profile.threshold = newThreshold;
|
|
76
|
+
profile.adjustmentCount++;
|
|
77
|
+
|
|
78
|
+
const record = {
|
|
79
|
+
score,
|
|
80
|
+
oldThreshold,
|
|
81
|
+
newThreshold,
|
|
82
|
+
signals,
|
|
83
|
+
timestamp: new Date().toISOString(),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
profile.history.push(record);
|
|
87
|
+
if (profile.history.length > 100) profile.history = profile.history.slice(-100);
|
|
88
|
+
|
|
89
|
+
// Check convergence
|
|
90
|
+
profile.converged = this.checkConvergence(profile);
|
|
91
|
+
|
|
92
|
+
this.save();
|
|
93
|
+
return { score, threshold: newThreshold, converged: profile.converged };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
scoreSession(signals) {
|
|
97
|
+
// Score 0-100 based on quality signals.
|
|
98
|
+
// Low score → rotate sooner (threshold decreases)
|
|
99
|
+
// High score → allow more context (threshold increases)
|
|
100
|
+
let score = 70; // Baseline: decent session
|
|
101
|
+
|
|
102
|
+
// Error rate: each error costs 5 points
|
|
103
|
+
const errorCount = signals.errorCount || 0;
|
|
104
|
+
score -= errorCount * 5;
|
|
105
|
+
|
|
106
|
+
// Repetitions: each detected repetition costs 8 points (agent repeating itself)
|
|
107
|
+
const repetitions = signals.repetitions || 0;
|
|
108
|
+
score -= repetitions * 8;
|
|
109
|
+
|
|
110
|
+
// Out-of-scope access: each violation costs 10 points
|
|
111
|
+
const scopeViolations = signals.scopeViolations || 0;
|
|
112
|
+
score -= scopeViolations * 10;
|
|
113
|
+
|
|
114
|
+
// Tool call success rate: bonus for high success, penalty for failures
|
|
115
|
+
const toolCalls = signals.toolCalls || 0;
|
|
116
|
+
const toolFailures = signals.toolFailures || 0;
|
|
117
|
+
if (toolCalls > 0) {
|
|
118
|
+
const successRate = (toolCalls - toolFailures) / toolCalls;
|
|
119
|
+
score += Math.round((successRate - 0.8) * 20); // Bonus/penalty around 80%
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// File churn: editing same file 3+ times signals circular refactoring
|
|
123
|
+
const fileChurn = signals.fileChurn || 0;
|
|
124
|
+
score -= fileChurn * 12;
|
|
125
|
+
|
|
126
|
+
// Error trend: increasing errors in second half = degradation
|
|
127
|
+
const errorTrend = signals.errorTrend || 0;
|
|
128
|
+
if (errorTrend > 0) score -= errorTrend * 6; // getting worse
|
|
129
|
+
if (errorTrend < 0) score += 3; // getting better (small bonus)
|
|
130
|
+
|
|
131
|
+
// Files written: productivity signal
|
|
132
|
+
const filesWritten = signals.filesWritten || 0;
|
|
133
|
+
score += Math.min(filesWritten * 2, 10); // Cap at +10
|
|
134
|
+
|
|
135
|
+
// Clamp to 0-100
|
|
136
|
+
return Math.max(0, Math.min(100, score));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
checkConvergence(profile) {
|
|
140
|
+
const history = profile.history;
|
|
141
|
+
if (history.length < CONVERGENCE_WINDOW) return false;
|
|
142
|
+
|
|
143
|
+
const recent = history.slice(-CONVERGENCE_WINDOW);
|
|
144
|
+
const maxDelta = Math.max(
|
|
145
|
+
...recent.map((r) => Math.abs(r.newThreshold - r.oldThreshold))
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return maxDelta < 0.01; // All recent adjustments < 1%
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Extract quality signals from classifier events and filtered log entries.
|
|
152
|
+
// These signals drive the scoring model that adapts rotation thresholds.
|
|
153
|
+
extractSignals(entries, agentScope) {
|
|
154
|
+
const signals = {
|
|
155
|
+
errorCount: 0,
|
|
156
|
+
repetitions: 0,
|
|
157
|
+
scopeViolations: 0,
|
|
158
|
+
toolCalls: 0,
|
|
159
|
+
toolFailures: 0,
|
|
160
|
+
filesWritten: 0,
|
|
161
|
+
fileChurn: 0, // same file written 3+ times → possible circular refactoring
|
|
162
|
+
errorTrend: 0, // errors increasing in recent window → degradation signal
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const recentTools = [];
|
|
166
|
+
const writtenFiles = new Set();
|
|
167
|
+
const fileWriteCounts = {}; // track how many times each file is written
|
|
168
|
+
const errorTimestamps = [];
|
|
169
|
+
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
if (entry.type === 'error') {
|
|
172
|
+
signals.errorCount++;
|
|
173
|
+
errorTimestamps.push(entry.timestamp || Date.now());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (entry.type === 'tool') {
|
|
177
|
+
signals.toolCalls++;
|
|
178
|
+
|
|
179
|
+
// Track file writes and churn
|
|
180
|
+
if (entry.tool === 'Write' || entry.tool === 'Edit') {
|
|
181
|
+
if (entry.input) {
|
|
182
|
+
writtenFiles.add(entry.input);
|
|
183
|
+
fileWriteCounts[entry.input] = (fileWriteCounts[entry.input] || 0) + 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Track tool failures — errors that follow tool calls indicate failure
|
|
188
|
+
// (stream-json emits tool_use then tool_result with is_error)
|
|
189
|
+
if (entry.type === 'tool' && entry.isError) {
|
|
190
|
+
signals.toolFailures++;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Detect repetitions: same tool+input within last 5 calls
|
|
194
|
+
const key = `${entry.tool}:${entry.input}`;
|
|
195
|
+
if (recentTools.includes(key)) {
|
|
196
|
+
signals.repetitions++;
|
|
197
|
+
}
|
|
198
|
+
recentTools.push(key);
|
|
199
|
+
if (recentTools.length > 5) recentTools.shift();
|
|
200
|
+
|
|
201
|
+
// Detect scope violations (simplified check)
|
|
202
|
+
if (agentScope && agentScope.length > 0 && entry.input) {
|
|
203
|
+
const file = entry.input;
|
|
204
|
+
if (entry.tool === 'Write' || entry.tool === 'Edit') {
|
|
205
|
+
const inScope = agentScope.some((pattern) =>
|
|
206
|
+
file.includes(pattern.replace('/**', '').replace('**/', ''))
|
|
207
|
+
);
|
|
208
|
+
if (!inScope) signals.scopeViolations++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
signals.filesWritten = writtenFiles.size;
|
|
215
|
+
|
|
216
|
+
// File churn: count files written 3+ times (circular refactoring signal)
|
|
217
|
+
for (const count of Object.values(fileWriteCounts)) {
|
|
218
|
+
if (count >= 3) signals.fileChurn++;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Error trend: compare error rate in first half vs second half of session
|
|
222
|
+
// Increasing errors = degradation signal
|
|
223
|
+
if (entries.length >= 6) {
|
|
224
|
+
const mid = Math.floor(entries.length / 2);
|
|
225
|
+
const firstHalfErrors = entries.slice(0, mid).filter((e) => e.type === 'error').length;
|
|
226
|
+
const secondHalfErrors = entries.slice(mid).filter((e) => e.type === 'error').length;
|
|
227
|
+
// Positive = errors increasing (bad), negative = errors decreasing (good)
|
|
228
|
+
signals.errorTrend = secondHalfErrors - firstHalfErrors;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return signals;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getAllProfiles() {
|
|
235
|
+
return this.profiles;
|
|
236
|
+
}
|
|
237
|
+
}
|