groove-dev 0.27.111 → 0.27.113

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 (54) hide show
  1. package/TRAINING_DATA_v3.md +11 -0
  2. package/codex-test/offroad-nitro-racer/dist/assets/index-CuvdKK6U.js +44 -0
  3. package/codex-test/offroad-nitro-racer/dist/assets/index-DvHn2Thu.css +1 -0
  4. package/codex-test/offroad-nitro-racer/dist/index.html +23 -0
  5. package/codex-test/offroad-nitro-racer/index.html +21 -0
  6. package/codex-test/offroad-nitro-racer/package-lock.json +841 -0
  7. package/codex-test/offroad-nitro-racer/package.json +15 -0
  8. package/codex-test/offroad-nitro-racer/src/game/AI.ts +28 -0
  9. package/codex-test/offroad-nitro-racer/src/game/Audio.ts +63 -0
  10. package/codex-test/offroad-nitro-racer/src/game/Car.ts +247 -0
  11. package/codex-test/offroad-nitro-racer/src/game/Effects.ts +62 -0
  12. package/codex-test/offroad-nitro-racer/src/game/Game.ts +229 -0
  13. package/codex-test/offroad-nitro-racer/src/game/Input.ts +45 -0
  14. package/codex-test/offroad-nitro-racer/src/game/Renderer.ts +224 -0
  15. package/codex-test/offroad-nitro-racer/src/game/Track.ts +158 -0
  16. package/codex-test/offroad-nitro-racer/src/game/UI.ts +96 -0
  17. package/codex-test/offroad-nitro-racer/src/game/math.ts +42 -0
  18. package/codex-test/offroad-nitro-racer/src/main.ts +24 -0
  19. package/codex-test/offroad-nitro-racer/src/style.css +291 -0
  20. package/codex-test/offroad-nitro-racer/src/vite-env.d.ts +1 -0
  21. package/codex-test/offroad-nitro-racer/tsconfig.json +18 -0
  22. package/codex-test/offroad-nitro-racer/vite.config.ts +7 -0
  23. package/moe-training/client/consent.js +47 -55
  24. package/moe-training/client/parsers/codex.js +3 -3
  25. package/moe-training/client/parsers/gemini.js +2 -2
  26. package/moe-training/client/step-classifier.js +2 -2
  27. package/moe-training/test/client/consent.test.js +23 -20
  28. package/moe-training/test/client/step-classifier.test.js +63 -7
  29. package/node_modules/@groove-dev/cli/package.json +1 -1
  30. package/node_modules/@groove-dev/daemon/package.json +1 -1
  31. package/node_modules/@groove-dev/daemon/src/api.js +74 -69
  32. package/node_modules/@groove-dev/daemon/src/index.js +30 -18
  33. package/node_modules/@groove-dev/gui/dist/assets/{index-CHu5w3i3.js → index-BYh6iHqL.js} +3 -3
  34. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  35. package/node_modules/@groove-dev/gui/package.json +1 -1
  36. package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +3 -1
  37. package/node_modules/@groove-dev/gui/src/stores/groove.js +15 -0
  38. package/node_modules/moe-training/client/consent.js +47 -55
  39. package/node_modules/moe-training/client/parsers/codex.js +3 -3
  40. package/node_modules/moe-training/client/parsers/gemini.js +2 -2
  41. package/node_modules/moe-training/client/step-classifier.js +2 -2
  42. package/node_modules/moe-training/test/client/consent.test.js +23 -20
  43. package/node_modules/moe-training/test/client/step-classifier.test.js +63 -7
  44. package/package.json +1 -1
  45. package/packages/cli/package.json +1 -1
  46. package/packages/daemon/package.json +1 -1
  47. package/packages/daemon/src/api.js +74 -69
  48. package/packages/daemon/src/index.js +30 -18
  49. package/packages/gui/dist/assets/{index-CHu5w3i3.js → index-BYh6iHqL.js} +3 -3
  50. package/packages/gui/dist/index.html +1 -1
  51. package/packages/gui/package.json +1 -1
  52. package/packages/gui/src/components/preview/preview-workspace.jsx +3 -1
  53. package/packages/gui/src/stores/groove.js +15 -0
  54. package/TRAINING_DATA_v2.md +0 -9
@@ -6,7 +6,7 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <link rel="icon" type="image/png" href="/favicon.png" />
8
8
  <title>Groove GUI</title>
9
- <script type="module" crossorigin src="/assets/index-CHu5w3i3.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-BYh6iHqL.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/codemirror-CFF1Lrnz.js">
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.111",
3
+ "version": "0.27.113",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -237,7 +237,9 @@ export function PreviewWorkspace({ embedded = false }) {
237
237
  return <EmptyPreview />;
238
238
  }
239
239
 
240
- const iframeSrc = previewState.url;
240
+ const iframeSrc = previewState.teamId
241
+ ? `/api/preview/${previewState.teamId}/proxy/`
242
+ : previewState.url;
241
243
 
242
244
  const deviceWidth = DEVICE_WIDTHS[previewState.deviceSize] || '100%';
243
245
  const isFullWidth = previewState.deviceSize === 'desktop';
@@ -224,6 +224,7 @@ export const useGrooveStore = create((set, get) => ({
224
224
  get().fetchBetaStatus();
225
225
  get().fetchNetworkInstallStatus();
226
226
  get().fetchTrainingStatus();
227
+ get().fetchActivePreviews();
227
228
  if (!get().onboardingComplete) get().fetchOnboardingStatus();
228
229
  if (window.groove?.auth?.onSubscriptionStatus) {
229
230
  window.groove.auth.onSubscriptionStatus((data) => {
@@ -1261,6 +1262,20 @@ export const useGrooveStore = create((set, get) => ({
1261
1262
 
1262
1263
  // ── Preview ──────────────────────────────────────────────
1263
1264
 
1265
+ async fetchActivePreviews() {
1266
+ try {
1267
+ const data = await api.get('/preview');
1268
+ const previews = data.previews || [];
1269
+ if (previews.length > 0) {
1270
+ const p = previews.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0))[0];
1271
+ set({
1272
+ previewState: { url: `/api/preview/${p.teamId}/proxy/`, teamId: p.teamId, kind: p.kind, deviceSize: 'desktop', screenshotMode: false },
1273
+ showPreviewInAgents: true,
1274
+ });
1275
+ }
1276
+ } catch {}
1277
+ },
1278
+
1264
1279
  openPreview(url, teamId, kind) {
1265
1280
  set({ previewState: { url, teamId, kind, deviceSize: 'desktop', screenshotMode: false }, previewChat: [], showPreviewInAgents: true });
1266
1281
  },
@@ -1,44 +1,46 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
 
3
- import Database from 'better-sqlite3';
4
3
  import { randomUUID } from 'node:crypto';
5
- import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
6
5
  import { join } from 'node:path';
7
6
  import { homedir } from 'node:os';
8
7
  import { CURRENT_CONSENT_VERSION } from '../shared/constants.js';
9
8
 
9
+ function ensureDir(filePath) {
10
+ const dir = filePath.replace(/[/\\][^/\\]+$/, '');
11
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
12
+ }
13
+
14
+ function readJSON(filePath) {
15
+ if (!existsSync(filePath)) return null;
16
+ try { return JSON.parse(readFileSync(filePath, 'utf-8')); } catch { return null; }
17
+ }
18
+
19
+ function writeJSON(filePath, data) {
20
+ ensureDir(filePath);
21
+ writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
22
+ }
23
+
10
24
  export class ConsentManager {
11
- constructor(dbPath) {
12
- this._dbPath = dbPath || join(homedir(), '.groove', 'consent.db');
13
- const dir = this._dbPath.replace(/[/\\][^/\\]+$/, '');
14
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
15
- this._db = new Database(this._dbPath);
16
- this._db.pragma('journal_mode = WAL');
17
- this._db.exec(`
18
- CREATE TABLE IF NOT EXISTS consent_history (
19
- id INTEGER PRIMARY KEY AUTOINCREMENT,
20
- user_id TEXT NOT NULL,
21
- opted_in INTEGER NOT NULL,
22
- consent_version TEXT NOT NULL,
23
- metadata TEXT,
24
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
25
- )
26
- `);
25
+ constructor(consentPath) {
26
+ this._path = consentPath || join(homedir(), '.groove', 'consent.json');
27
27
  }
28
28
 
29
- recordConsent(userId, optedIn, consentVersion, metadata) {
30
- this._db.prepare(
31
- 'INSERT INTO consent_history (user_id, opted_in, consent_version, metadata) VALUES (?, ?, ?, ?)'
32
- ).run(userId, optedIn ? 1 : 0, consentVersion, metadata ? JSON.stringify(metadata) : null);
29
+ recordConsent(userId, optedIn, consentVersion) {
30
+ const data = {
31
+ user_id: userId,
32
+ opted_in: !!optedIn,
33
+ consent_version: consentVersion,
34
+ updated_at: new Date().toISOString(),
35
+ };
36
+ writeJSON(this._path, data);
33
37
  }
34
38
 
35
39
  isOptedIn(userId) {
36
- const row = this._db.prepare(
37
- 'SELECT opted_in, consent_version FROM consent_history WHERE user_id = ? ORDER BY id DESC LIMIT 1'
38
- ).get(userId);
39
- if (!row) return false;
40
- if (row.consent_version !== CURRENT_CONSENT_VERSION) return false;
41
- return row.opted_in === 1;
40
+ const data = readJSON(this._path);
41
+ if (!data) return false;
42
+ if (data.consent_version !== CURRENT_CONSENT_VERSION) return false;
43
+ return data.opted_in === true;
42
44
  }
43
45
 
44
46
  revokeConsent(userId) {
@@ -46,34 +48,28 @@ export class ConsentManager {
46
48
  }
47
49
 
48
50
  getOptedInCount() {
49
- const row = this._db.prepare(`
50
- SELECT COUNT(DISTINCT user_id) as cnt FROM consent_history ch1
51
- WHERE opted_in = 1
52
- AND consent_version = ?
53
- AND id = (SELECT MAX(id) FROM consent_history ch2 WHERE ch2.user_id = ch1.user_id)
54
- `).get(CURRENT_CONSENT_VERSION);
55
- return row?.cnt || 0;
51
+ const data = readJSON(this._path);
52
+ if (!data || !data.opted_in || data.consent_version !== CURRENT_CONSENT_VERSION) return 0;
53
+ return 1;
56
54
  }
57
55
 
58
56
  getConsentHistory(userId) {
59
- const rows = this._db.prepare(
60
- 'SELECT * FROM consent_history WHERE user_id = ? ORDER BY id ASC'
61
- ).all(userId);
62
- return rows.map((r) => ({
63
- ...r,
64
- opted_in: r.opted_in === 1,
65
- metadata: r.metadata ? JSON.parse(r.metadata) : null,
66
- }));
57
+ const data = readJSON(this._path);
58
+ if (!data) return [];
59
+ return [{
60
+ user_id: data.user_id,
61
+ opted_in: data.opted_in,
62
+ consent_version: data.consent_version,
63
+ created_at: data.updated_at,
64
+ metadata: null,
65
+ }];
67
66
  }
68
67
 
69
- close() {
70
- this._db.close();
71
- }
68
+ close() {}
72
69
 
73
70
  static getOrCreateUserId(userIdPath) {
74
71
  const filePath = userIdPath || join(homedir(), '.groove', 'user_id');
75
- const dir = filePath.replace(/[/\\][^/\\]+$/, '');
76
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
72
+ ensureDir(filePath);
77
73
  if (existsSync(filePath)) {
78
74
  return readFileSync(filePath, 'utf-8').trim();
79
75
  }
@@ -82,15 +78,11 @@ export class ConsentManager {
82
78
  return uid;
83
79
  }
84
80
 
85
- static isCaptureEnabled(userIdPath, dbPath) {
81
+ static isCaptureEnabled(userIdPath, consentPath) {
86
82
  const filePath = userIdPath || join(homedir(), '.groove', 'user_id');
87
83
  if (!existsSync(filePath)) return false;
84
+ const manager = new ConsentManager(consentPath);
88
85
  const userId = readFileSync(filePath, 'utf-8').trim();
89
- const manager = new ConsentManager(dbPath);
90
- try {
91
- return manager.isOptedIn(userId);
92
- } finally {
93
- manager.close();
94
- }
86
+ return manager.isOptedIn(userId);
95
87
  }
96
88
  }
@@ -57,15 +57,15 @@ export class CodexParser {
57
57
  if (item.type === 'command_execution') {
58
58
  const rawOutput = item.aggregated_output || '';
59
59
  if (item.exit_code !== 0) {
60
- return { type: 'error', content: rawOutput.slice(0, 2000) || `Exit code: ${item.exit_code}` };
60
+ return { type: 'error', is_error: true, content: rawOutput.slice(0, 2000) || `Exit code: ${item.exit_code}` };
61
61
  }
62
62
  const obs = truncateObservation(rawOutput);
63
- return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
63
+ return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
64
64
  }
65
65
  if (item.type === 'file_edit' || item.type === 'file_write' || item.type === 'file_read') {
66
66
  const rawOutput = item.output || item.content || '';
67
67
  const obs = truncateObservation(rawOutput);
68
- return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
68
+ return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
69
69
  }
70
70
  return null;
71
71
  }
@@ -64,11 +64,11 @@ export class GeminiParser {
64
64
  const contentParts = Array.isArray(rawContent) ? rawContent : (typeof rawContent === 'string' ? [{ text: rawContent }] : rawContent ? [rawContent] : []);
65
65
  const rawText = contentParts.map((p) => p.text || '').join('');
66
66
  const obs = truncateObservation(rawText);
67
- return { type: 'observation', content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
67
+ return { type: 'observation', is_error: false, content: obs.content, truncated: obs.truncated, original_token_count: obs.original_token_count };
68
68
  }
69
69
 
70
70
  case 'error': {
71
- return { type: 'error', content: jsonEvent.message || 'Unknown error' };
71
+ return { type: 'error', is_error: true, content: jsonEvent.message || 'Unknown error' };
72
72
  }
73
73
 
74
74
  case 'agent_end': {
@@ -1,6 +1,6 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
 
3
- const ERROR_SIGNAL_RE = /\b(?:error|Error|ERROR|exception|Exception|EXCEPTION|failed|FAILED|exit code [1-9]|ENOENT|EACCES|EPERM|TypeError|ReferenceError|SyntaxError|Cannot find|Module not found|Command failed|non-zero exit)\b/;
3
+ const ERROR_SIGNAL_RE = /(?:^|\n)\s*(?:error[ :\[]\S|Error[ :\[]\S|ERROR[ :\[]\S)|(?:exit code [1-9]|non-zero exit|ENOENT|EACCES|EPERM|Command failed)|(?:(?:TypeError|ReferenceError|SyntaxError|RangeError|URIError):\s)|(?:Cannot find module|Module not found|ModuleNotFoundError|ImportError:)|(?:FATAL|PANIC|Traceback \(most recent)|(?:error TS\d{4}:)/;
4
4
  const FIX_SIGNAL_RE = /\b(?:fix|correcting|I see the issue|let me fix|the (?:issue|problem|bug) (?:is|was)|instead I should|my mistake)\b/i;
5
5
 
6
6
  const CORRECTION_RE = /\b(?:no[,. ](?:that|not|don't|wrong)|that'?s (?:not|wrong|incorrect)|don'?t do that|stop (?:doing|that)|instead (?:of|do)|undo|revert|go back|try (?:again|differently)|you (?:broke|missed|forgot))\b/i;
@@ -54,7 +54,7 @@ export class StepClassifier {
54
54
 
55
55
  const content = step.content || '';
56
56
 
57
- if ((step.type === 'action' || step.type === 'observation') && step.is_error !== false && ERROR_SIGNAL_RE.test(content)) {
57
+ if (step.type === 'observation' && step.is_error !== false && ERROR_SIGNAL_RE.test(content)) {
58
58
  step.type = 'error';
59
59
  }
60
60
 
@@ -8,12 +8,12 @@ import { tmpdir } from 'node:os';
8
8
  import { ConsentManager } from '../../client/consent.js';
9
9
 
10
10
  describe('ConsentManager', () => {
11
- let tmpDir, dbPath, manager;
11
+ let tmpDir, consentPath, manager;
12
12
 
13
13
  beforeEach(() => {
14
14
  tmpDir = mkdtempSync(join(tmpdir(), 'consent-test-'));
15
- dbPath = join(tmpDir, 'consent.db');
16
- manager = new ConsentManager(dbPath);
15
+ consentPath = join(tmpDir, 'consent.json');
16
+ manager = new ConsentManager(consentPath);
17
17
  });
18
18
 
19
19
  afterEach(() => {
@@ -43,19 +43,24 @@ describe('ConsentManager', () => {
43
43
 
44
44
  it('getOptedInCount counts correctly', () => {
45
45
  manager.recordConsent('user1', true, '1.0');
46
- manager.recordConsent('user2', true, '1.0');
47
- manager.recordConsent('user3', false, '1.0');
48
- assert.equal(manager.getOptedInCount(), 2);
46
+ assert.equal(manager.getOptedInCount(), 1);
49
47
  });
50
48
 
51
- it('getConsentHistory returns all records', () => {
52
- manager.recordConsent('user1', true, '1.0', { source: 'ui' });
53
- manager.revokeConsent('user1');
49
+ it('getConsentHistory returns current state', () => {
50
+ manager.recordConsent('user1', true, '1.0');
54
51
  const history = manager.getConsentHistory('user1');
55
- assert.equal(history.length, 2);
52
+ assert.equal(history.length, 1);
56
53
  assert.equal(history[0].opted_in, true);
57
- assert.deepEqual(history[0].metadata, { source: 'ui' });
58
- assert.equal(history[1].opted_in, false);
54
+ });
55
+
56
+ it('consent.json is written with 0o600 permissions', () => {
57
+ manager.recordConsent('user1', true, '1.0');
58
+ assert.ok(existsSync(consentPath));
59
+ const data = JSON.parse(readFileSync(consentPath, 'utf-8'));
60
+ assert.equal(data.opted_in, true);
61
+ assert.equal(data.consent_version, '1.0');
62
+ assert.equal(data.user_id, 'user1');
63
+ assert.ok(data.updated_at);
59
64
  });
60
65
  });
61
66
 
@@ -96,26 +101,24 @@ describe('ConsentManager.isCaptureEnabled', () => {
96
101
  });
97
102
 
98
103
  it('returns false when no user_id file exists', () => {
99
- const result = ConsentManager.isCaptureEnabled(join(tmpDir, 'no_file'), join(tmpDir, 'c.db'));
104
+ const result = ConsentManager.isCaptureEnabled(join(tmpDir, 'no_file'), join(tmpDir, 'consent.json'));
100
105
  assert.equal(result, false);
101
106
  });
102
107
 
103
108
  it('returns false when user not opted in', () => {
104
109
  const uidPath = join(tmpDir, 'user_id');
105
- const uid = ConsentManager.getOrCreateUserId(uidPath);
106
- const dbPath = join(tmpDir, 'consent.db');
107
- const result = ConsentManager.isCaptureEnabled(uidPath, dbPath);
110
+ ConsentManager.getOrCreateUserId(uidPath);
111
+ const result = ConsentManager.isCaptureEnabled(uidPath, join(tmpDir, 'consent.json'));
108
112
  assert.equal(result, false);
109
113
  });
110
114
 
111
115
  it('returns true when user is opted in', () => {
112
116
  const uidPath = join(tmpDir, 'user_id');
113
117
  const uid = ConsentManager.getOrCreateUserId(uidPath);
114
- const dbPath = join(tmpDir, 'consent.db');
115
- const mgr = new ConsentManager(dbPath);
118
+ const consentPath = join(tmpDir, 'consent.json');
119
+ const mgr = new ConsentManager(consentPath);
116
120
  mgr.recordConsent(uid, true, '1.0');
117
- mgr.close();
118
- const result = ConsentManager.isCaptureEnabled(uidPath, dbPath);
121
+ const result = ConsentManager.isCaptureEnabled(uidPath, consentPath);
119
122
  assert.equal(result, true);
120
123
  });
121
124
  });
@@ -132,11 +132,11 @@ describe('StepClassifier', () => {
132
132
  assert.equal(StepClassifier.countUserInterventions(steps), 0);
133
133
  });
134
134
 
135
- it('reclassifies action with error content to error', () => {
135
+ it('never reclassifies action to error', () => {
136
136
  const classifier = new StepClassifier();
137
137
  const step = { type: 'action', content: 'Command failed with exit code 1' };
138
138
  const result = classifier.onStep(step);
139
- assert.equal(result.type, 'error');
139
+ assert.equal(result.type, 'action');
140
140
  });
141
141
 
142
142
  it('reclassifies observation with error content to error', () => {
@@ -215,19 +215,75 @@ describe('StepClassifier', () => {
215
215
  assert.equal(result.type, 'error');
216
216
  });
217
217
 
218
- it('preserves action type when is_error is false', () => {
218
+ it('does not reclassify observation containing bare word "error" in source code', () => {
219
219
  const classifier = new StepClassifier();
220
- const step = { type: 'action', content: 'Command failed with exit code 1', is_error: false };
220
+ const step = { type: 'observation', content: 'function handleError(err) { console.error(err); }' };
221
221
  const result = classifier.onStep(step);
222
- assert.equal(result.type, 'action');
222
+ assert.equal(result.type, 'observation');
223
223
  });
224
224
 
225
- it('still reclassifies action to error when is_error is not set', () => {
225
+ it('does not reclassify observation with "0 errors" or "found 0 vulnerabilities"', () => {
226
226
  const classifier = new StepClassifier();
227
- const step = { type: 'action', content: 'Command failed with exit code 1' };
227
+ const step = { type: 'observation', content: 'Build succeeded\n0 errors, 0 warnings\nfound 0 vulnerabilities' };
228
+ const result = classifier.onStep(step);
229
+ assert.equal(result.type, 'observation');
230
+ });
231
+
232
+ it('does not reclassify observation reading a file that mentions exceptions', () => {
233
+ const classifier = new StepClassifier();
234
+ const step = { type: 'observation', content: '{"scripts": {"build": "tsc && vite build"}, "name": "my-app"}' };
235
+ const result = classifier.onStep(step);
236
+ assert.equal(result.type, 'observation');
237
+ });
238
+
239
+ it('reclassifies observation with real TypeScript build error', () => {
240
+ const classifier = new StepClassifier();
241
+ const step = { type: 'observation', content: 'src/main.ts(1,8): error TS2882: Cannot find module' };
242
+ const result = classifier.onStep(step);
243
+ assert.equal(result.type, 'error');
244
+ });
245
+
246
+ it('reclassifies observation with Python traceback', () => {
247
+ const classifier = new StepClassifier();
248
+ const step = { type: 'observation', content: 'Traceback (most recent call last):\n File "main.py", line 5' };
249
+ const result = classifier.onStep(step);
250
+ assert.equal(result.type, 'error');
251
+ });
252
+
253
+ it('reclassifies observation with actual TypeError message', () => {
254
+ const classifier = new StepClassifier();
255
+ const step = { type: 'observation', content: 'TypeError: Cannot read properties of undefined (reading "map")' };
256
+ const result = classifier.onStep(step);
257
+ assert.equal(result.type, 'error');
258
+ });
259
+
260
+ it('reclassifies observation with exit code failure', () => {
261
+ const classifier = new StepClassifier();
262
+ const step = { type: 'observation', content: 'Process exited with exit code 1' };
263
+ const result = classifier.onStep(step);
264
+ assert.equal(result.type, 'error');
265
+ });
266
+
267
+ it('reclassifies observation with ModuleNotFoundError', () => {
268
+ const classifier = new StepClassifier();
269
+ const step = { type: 'observation', content: 'ModuleNotFoundError: No module named requests' };
228
270
  const result = classifier.onStep(step);
229
271
  assert.equal(result.type, 'error');
230
272
  });
273
+
274
+ it('preserves action type even with error keywords', () => {
275
+ const classifier = new StepClassifier();
276
+ const step = { type: 'action', content: 'Command failed with exit code 1' };
277
+ const result = classifier.onStep(step);
278
+ assert.equal(result.type, 'action');
279
+ });
280
+
281
+ it('preserves action type regardless of is_error flag', () => {
282
+ const classifier = new StepClassifier();
283
+ const step = { type: 'action', content: 'ENOENT: no such file', is_error: true };
284
+ const result = classifier.onStep(step);
285
+ assert.equal(result.type, 'action');
286
+ });
231
287
  });
232
288
 
233
289
  describe('StepClassifier.classifyIntent', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.111",
3
+ "version": "0.27.113",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.111",
3
+ "version": "0.27.113",
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.111",
3
+ "version": "0.27.113",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",