groove-dev 0.27.39 → 0.27.40

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.39",
3
+ "version": "0.27.40",
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.39",
3
+ "version": "0.27.40",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -2868,23 +2868,28 @@ Keep responses concise. Help them think, don't lecture them about the system the
2868
2868
  res.json(result);
2869
2869
  });
2870
2870
 
2871
- // Clean up stale artifacts (old plans, recommended teams, etc.)
2871
+ // Clean up stale artifacts. Scope to a single team when teamId is provided —
2872
+ // wiping every agent's working dir on a global cleanup would delete other
2873
+ // in-flight teams' unlaunched plans. When called with no teamId, only the
2874
+ // daemon-root plan file is touched (safe baseline).
2872
2875
  app.post('/api/cleanup', (req, res) => {
2876
+ const teamId = req.body?.teamId || req.query?.teamId || null;
2873
2877
  let cleaned = 0;
2874
- // Clean recommended-team.json from all known locations
2875
2878
  const locations = [resolve(daemon.grooveDir, 'recommended-team.json')];
2876
- for (const agent of daemon.registry.getAll()) {
2877
- if (agent.workingDir) {
2878
- locations.push(resolve(agent.workingDir, '.groove', 'recommended-team.json'));
2879
+
2880
+ if (teamId) {
2881
+ // Only agents in this team get their workspace scanned
2882
+ for (const agent of daemon.registry.getAll()) {
2883
+ if (agent.teamId === teamId && agent.workingDir) {
2884
+ locations.push(resolve(agent.workingDir, '.groove', 'recommended-team.json'));
2885
+ }
2879
2886
  }
2880
2887
  }
2881
- const defaultDir = daemon.config?.defaultWorkingDir;
2882
- if (defaultDir) locations.push(resolve(defaultDir, '.groove', 'recommended-team.json'));
2883
2888
 
2884
2889
  for (const p of locations) {
2885
2890
  if (existsSync(p)) { try { unlinkSync(p); cleaned++; } catch { /* */ } }
2886
2891
  }
2887
- daemon.audit.log('cleanup', { cleaned });
2892
+ daemon.audit.log('cleanup', { cleaned, teamId });
2888
2893
  res.json({ ok: true, cleaned });
2889
2894
  });
2890
2895
 
@@ -19,6 +19,11 @@ import { lookup as mimeLookup } from './mimetypes.js';
19
19
 
20
20
  const READY_TIMEOUT_MS = 60_000; // give dev servers a minute to boot
21
21
  const MAX_STDOUT_BYTES = 256 * 1024;
22
+ // Strip CSI/OSC/other ANSI escape sequences — Vite prints URLs with inline
23
+ // bold/color codes (e.g. "http://localhost:\x1b[1m5175\x1b[22m/") which would
24
+ // otherwise break port-number regexes.
25
+ const ANSI_REGEX = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
26
+ function stripAnsi(s) { return s.replace(ANSI_REGEX, ''); }
22
27
 
23
28
  export class PreviewService {
24
29
  constructor(daemon) {
@@ -168,11 +173,12 @@ export class PreviewService {
168
173
  };
169
174
 
170
175
  const timer = setTimeout(() => {
171
- finish({ launched: false, reason: `timeout waiting for url in stdout; last stderr: ${stderrBuf.slice(-400)}` });
176
+ const tail = stripAnsi(stderrBuf).slice(-400) || stripAnsi(stdoutBuf).slice(-400) || '(no output)';
177
+ finish({ launched: false, reason: `timeout waiting for url in stdout; last output: ${tail}` });
172
178
  }, READY_TIMEOUT_MS);
173
179
 
174
180
  const tryMatch = () => {
175
- const combined = stdoutBuf + '\n' + stderrBuf;
181
+ const combined = stripAnsi(stdoutBuf + '\n' + stderrBuf);
176
182
  if (readyText && !combined.includes(readyText)) return;
177
183
  const m = combined.match(urlPattern);
178
184
  if (!m) return;
@@ -10,8 +10,10 @@ const DEFAULT_THRESHOLD = 0.65; // For non-self-managing providers (was 0.7
10
10
  const HARD_CEILING = 0.80; // Force rotate (was 0.85) — only for non-self-managing
11
11
  const CHECK_INTERVAL = 15_000;
12
12
  const QUALITY_THRESHOLD = 40; // Score below this triggers quality rotation
13
- const MIN_EVENTS = 10; // Minimum classifier events before scoring
14
- const MIN_AGE_SEC = 120; // Minimum agent age before quality rotation
13
+ const MIN_EVENTS = 20; // Minimum classifier events before scoring
14
+ const MIN_AGE_SEC = 300; // Minimum agent age before quality rotation (5 min)
15
+ const QUALITY_MIN_TOKENS = 20_000; // Minimum real token work before quality rotation can fire
16
+ const QUALITY_MIN_FILES = 3; // Or: 3 successful file writes proves the agent is productive
15
17
  const SCORE_HISTORY_MAX = 40; // ~10 min at 15s intervals
16
18
  const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes between rotations per agent
17
19
  const QUALITY_COOLDOWN_MS = 2 * 60 * 1000; // 2 minutes for quality degradation rotations
@@ -128,7 +130,20 @@ export class Rotator extends EventEmitter {
128
130
  return { score: 70, signals: {}, hasEnoughData: false, ageSec: Math.round(ageSec), eventCount: events.length };
129
131
  }
130
132
 
131
- const signals = this.daemon.adaptive.extractSignals(events, agent.scope);
133
+ // Productive-work floor: don't even score an agent that hasn't produced
134
+ // enough to judge. A frontend agent scaffolding a project naturally emits
135
+ // noisy signals (npm install warnings, Write retries) in its first few
136
+ // minutes; killing it mid-scaffold destroys the context it was building.
137
+ // Only allow the score to gate rotation once EITHER substantial tokens
138
+ // have flowed OR the agent has already written multiple files successfully.
139
+ const tokens = agent.tokensUsed || 0;
140
+ const signalsEarly = this.daemon.adaptive.extractSignals(events, agent.scope);
141
+ const filesWritten = signalsEarly.filesWritten || 0;
142
+ if (tokens < QUALITY_MIN_TOKENS && filesWritten < QUALITY_MIN_FILES) {
143
+ return { score: 70, signals: signalsEarly, hasEnoughData: false, ageSec: Math.round(ageSec), eventCount: events.length, reason: 'below_productive_floor' };
144
+ }
145
+
146
+ const signals = signalsEarly;
132
147
  let score = this.daemon.adaptive.scoreSession(signals);
133
148
 
134
149
  if (ageSec > 1800) score -= 5;