groove-dev 0.27.113 → 0.27.115

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 (66) hide show
  1. package/CENTRAL_COMMAND_REBUILD.md +689 -0
  2. package/EMBEDDING_DIAGNOSTIC.md +197 -0
  3. package/TRAINING_DATA_v4.md +3 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/cli/src/commands/team.js +43 -1
  6. package/node_modules/@groove-dev/daemon/package.json +1 -1
  7. package/node_modules/@groove-dev/daemon/src/api.js +24 -0
  8. package/node_modules/@groove-dev/daemon/src/filewatcher.js +45 -0
  9. package/node_modules/@groove-dev/daemon/src/index.js +14 -2
  10. package/node_modules/@groove-dev/daemon/src/teams.js +100 -6
  11. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +75 -43
  12. package/node_modules/@groove-dev/gui/dist/assets/{index-BYh6iHqL.js → index-BKCiOUDb.js} +593 -593
  13. package/node_modules/@groove-dev/gui/dist/assets/index-D4Q72afD.css +1 -0
  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/workspace-mode.jsx +0 -22
  17. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +43 -45
  18. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +2 -1
  19. package/node_modules/@groove-dev/gui/src/stores/groove.js +42 -8
  20. package/node_modules/@groove-dev/gui/src/views/agents.jsx +31 -3
  21. package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -20
  22. package/node_modules/@groove-dev/gui/src/views/teams.jsx +106 -3
  23. package/package.json +1 -1
  24. package/packages/cli/package.json +1 -1
  25. package/packages/cli/src/commands/team.js +43 -1
  26. package/packages/daemon/package.json +1 -1
  27. package/packages/daemon/src/api.js +24 -0
  28. package/packages/daemon/src/filewatcher.js +45 -0
  29. package/packages/daemon/src/index.js +14 -2
  30. package/packages/daemon/src/teams.js +100 -6
  31. package/packages/daemon/src/tunnel-manager.js +75 -43
  32. package/packages/gui/dist/assets/{index-BYh6iHqL.js → index-BKCiOUDb.js} +593 -593
  33. package/packages/gui/dist/assets/index-D4Q72afD.css +1 -0
  34. package/packages/gui/dist/index.html +2 -2
  35. package/packages/gui/package.json +1 -1
  36. package/packages/gui/src/components/agents/workspace-mode.jsx +0 -22
  37. package/packages/gui/src/components/layout/status-bar.jsx +43 -45
  38. package/packages/gui/src/components/settings/quick-connect.jsx +2 -1
  39. package/packages/gui/src/stores/groove.js +42 -8
  40. package/packages/gui/src/views/agents.jsx +31 -3
  41. package/packages/gui/src/views/editor.jsx +1 -20
  42. package/packages/gui/src/views/teams.jsx +106 -3
  43. package/TRAINING_DATA_v3.md +0 -11
  44. package/codex-test/offroad-nitro-racer/dist/assets/index-CuvdKK6U.js +0 -44
  45. package/codex-test/offroad-nitro-racer/dist/assets/index-DvHn2Thu.css +0 -1
  46. package/codex-test/offroad-nitro-racer/dist/index.html +0 -23
  47. package/codex-test/offroad-nitro-racer/index.html +0 -21
  48. package/codex-test/offroad-nitro-racer/package-lock.json +0 -841
  49. package/codex-test/offroad-nitro-racer/package.json +0 -15
  50. package/codex-test/offroad-nitro-racer/src/game/AI.ts +0 -28
  51. package/codex-test/offroad-nitro-racer/src/game/Audio.ts +0 -63
  52. package/codex-test/offroad-nitro-racer/src/game/Car.ts +0 -247
  53. package/codex-test/offroad-nitro-racer/src/game/Effects.ts +0 -62
  54. package/codex-test/offroad-nitro-racer/src/game/Game.ts +0 -229
  55. package/codex-test/offroad-nitro-racer/src/game/Input.ts +0 -45
  56. package/codex-test/offroad-nitro-racer/src/game/Renderer.ts +0 -224
  57. package/codex-test/offroad-nitro-racer/src/game/Track.ts +0 -158
  58. package/codex-test/offroad-nitro-racer/src/game/UI.ts +0 -96
  59. package/codex-test/offroad-nitro-racer/src/game/math.ts +0 -42
  60. package/codex-test/offroad-nitro-racer/src/main.ts +0 -24
  61. package/codex-test/offroad-nitro-racer/src/style.css +0 -291
  62. package/codex-test/offroad-nitro-racer/src/vite-env.d.ts +0 -1
  63. package/codex-test/offroad-nitro-racer/tsconfig.json +0 -18
  64. package/codex-test/offroad-nitro-racer/vite.config.ts +0 -7
  65. package/node_modules/@groove-dev/gui/dist/assets/index-DAlSbVyK.css +0 -1
  66. package/packages/gui/dist/assets/index-DAlSbVyK.css +0 -1
@@ -0,0 +1,197 @@
1
+ # Embedding Service Diagnostic — Central Command Rebuild
2
+
3
+ ## Problem
4
+
5
+ After the Central Command rebuild, `session_embedding` is `null` on all SESSION_CLOSE envelopes and `domain_tags` show keyword-mode confidence (~0.067) instead of embedding-mode confidence (~0.3-0.5). Embeddings were working correctly before the rebuild (TRAINING_DATA_v3 had full 384-dim vectors on every session).
6
+
7
+ The endpoint responds to direct curl tests from a developer machine, but the Groove client is falling back to keyword mode during live sessions.
8
+
9
+ ## What "working" looked like (v3 data)
10
+
11
+ ```
12
+ domain_tags: primary=react_frontend (0.4932)
13
+ session_embedding: 384d, model=sentence-transformers/all-MiniLM-L6-v2, nonzero=377/384
14
+ ```
15
+
16
+ ## What's happening now (v4 data)
17
+
18
+ ```
19
+ domain_tags: primary=nlp_llm (0.0769) ← keyword mode
20
+ session_embedding: null ← embed() returns null in keyword mode
21
+ ```
22
+
23
+ ## How the client decides between keyword and embedding mode
24
+
25
+ On every agent spawn, the `DomainTagger.init()` runs this sequence:
26
+
27
+ ```
28
+ 1. POST to EMBEDDING_SERVICE_URL with {"input": "health check", "model": "sentence-transformers/all-MiniLM-L6-v2"}
29
+ 2. If response is 200 OK:
30
+ a. Switch to "http" mode
31
+ b. Call _buildCentroids() — embeds all 40 domain descriptions (40 sequential POST calls)
32
+ c. If all succeeds → ready, embedding mode active
33
+ 3. If ANYTHING fails (network error, non-200, timeout, _buildCentroids() error):
34
+ → Fall back to "keyword" mode silently
35
+ ```
36
+
37
+ The client **never logs** why it fell back — it catches all errors silently. So the failure is invisible.
38
+
39
+ ## Likely failure points (in order of probability)
40
+
41
+ ### 1. `_buildCentroids()` is failing
42
+
43
+ Even if the health probe succeeds, `init()` then makes **40 sequential embed calls** to build domain centroids. If any single one fails, the whole init throws and falls back to keyword mode. The entire block is wrapped in a try/catch:
44
+
45
+ ```javascript
46
+ try {
47
+ const res = await fetch(this._serviceUrl, { ... }); // health probe
48
+ if (res.ok) {
49
+ this._mode = this._leafRegistry?.length ? 'registry' : 'http';
50
+ await this._buildCentroids(); // ← 40 more calls, if this throws → keyword mode
51
+ this._ready = true;
52
+ return;
53
+ }
54
+ } catch {
55
+ // silently falls through to keyword mode
56
+ }
57
+ ```
58
+
59
+ **To check:** Look at Central Command logs during a Groove session. You should see ~41 POST requests to `/v1/embed` in quick succession (1 health probe + 40 centroid builds). If you see fewer, one is failing.
60
+
61
+ ### 2. Timeout under load
62
+
63
+ The health probe has a **5-second timeout**. The centroid embed calls have a **10-second timeout** each. If the ONNX model is cold or the server is under load, individual calls might be timing out.
64
+
65
+ **To check:** Add timing logs to the `/v1/embed` endpoint:
66
+ ```javascript
67
+ const start = Date.now();
68
+ const vector = await embed(text);
69
+ console.log(`[embed] ${Date.now() - start}ms "${text.slice(0, 50)}..."`);
70
+ ```
71
+
72
+ ### 3. Response format mismatch
73
+
74
+ The client parses the response as:
75
+ ```javascript
76
+ const data = await res.json();
77
+ const embedding = data?.data?.[0]?.embedding;
78
+ if (!Array.isArray(embedding)) {
79
+ // returns null — "Invalid embedding response format"
80
+ }
81
+ ```
82
+
83
+ The expected response is **exactly**:
84
+ ```json
85
+ {
86
+ "data": [
87
+ {
88
+ "embedding": [0.0123, -0.0456, ...384 floats],
89
+ "index": 0
90
+ }
91
+ ],
92
+ "model": "sentence-transformers/all-MiniLM-L6-v2"
93
+ }
94
+ ```
95
+
96
+ **To check:** Verify the response structure. Common mistakes:
97
+ - Returning `{ "embedding": [...] }` instead of `{ "data": [{ "embedding": [...] }] }`
98
+ - Returning the vector as a nested array `[[0.01, ...]]` instead of flat `[0.01, ...]`
99
+ - Returning Float32Array or typed array instead of plain JSON numbers
100
+
101
+ ### 4. CORS or request rejection
102
+
103
+ The Groove daemon makes requests from `localhost` (or wherever it runs). If the rebuilt server has stricter CORS, different firewall rules, or middleware that rejects requests without a specific User-Agent or Origin header, the fetch would fail.
104
+
105
+ **To check:** The original Central Command had `Access-Control-Allow-Origin: *`. Verify the rebuild has the same.
106
+
107
+ ### 5. URL path mismatch
108
+
109
+ The client hits exactly: `POST https://api.groovedev.ai/v1/embed`
110
+
111
+ **To check:** Verify the route is mounted at `/v1/embed`, not `/embed` or `/v1/embeddings` or `/api/v1/embed`.
112
+
113
+ ## Quick diagnostic commands
114
+
115
+ Run these on the Central Command server:
116
+
117
+ ```bash
118
+ # 1. Verify endpoint exists and responds correctly
119
+ curl -s -X POST http://localhost:8443/v1/embed \
120
+ -H "Content-Type: application/json" \
121
+ -d '{"input": "Python programming debugging testing"}' \
122
+ | python3 -c "
123
+ import sys, json
124
+ d = json.load(sys.stdin)
125
+ v = d.get('data', [{}])[0].get('embedding')
126
+ if v and isinstance(v, list):
127
+ print(f'OK: {len(v)}-dim vector, type={type(v[0]).__name__}')
128
+ else:
129
+ print(f'BROKEN: response structure = {json.dumps(d)[:200]}')"
130
+
131
+ # 2. Simulate the full init sequence (health probe + 40 centroids)
132
+ python3 -c "
133
+ import requests, time
134
+ url = 'http://localhost:8443/v1/embed'
135
+ domains = [
136
+ 'Python programming, debugging, testing with pytest',
137
+ 'TypeScript and Node.js backend, Express frameworks',
138
+ 'React frontend development, JSX components, hooks',
139
+ # ...add all 40 domain descriptions from domain-tagger.js
140
+ ]
141
+
142
+ # Health probe
143
+ start = time.time()
144
+ r = requests.post(url, json={'input': 'health check', 'model': 'sentence-transformers/all-MiniLM-L6-v2'}, timeout=5)
145
+ print(f'Health probe: {r.status_code} in {time.time()-start:.1f}s')
146
+
147
+ # Centroid batch
148
+ fails = 0
149
+ for i, desc in enumerate(domains):
150
+ try:
151
+ r = requests.post(url, json={'input': desc}, timeout=10)
152
+ d = r.json()
153
+ v = d.get('data', [{}])[0].get('embedding')
154
+ if not v or not isinstance(v, list) or len(v) != 384:
155
+ print(f' Domain {i}: BAD RESPONSE')
156
+ fails += 1
157
+ except Exception as e:
158
+ print(f' Domain {i}: FAILED - {e}')
159
+ fails += 1
160
+
161
+ print(f'Centroid build: {len(domains)-fails}/{len(domains)} succeeded, {fails} failed')
162
+ if fails > 0:
163
+ print('THIS IS WHY EMBEDDINGS ARE NULL - any failure causes full fallback to keyword mode')
164
+ "
165
+
166
+ # 3. Check response time under sequential load
167
+ for i in $(seq 1 10); do
168
+ time curl -s -X POST http://localhost:8443/v1/embed \
169
+ -H "Content-Type: application/json" \
170
+ -d "{\"input\": \"test embedding call $i\"}" > /dev/null
171
+ done
172
+
173
+ # 4. Check server logs during a live Groove session
174
+ # Start a Groove agent and watch for /v1/embed requests in the Central Command logs
175
+ ```
176
+
177
+ ## What success looks like
178
+
179
+ After fix, a SESSION_CLOSE envelope should show:
180
+ ```json
181
+ {
182
+ "metadata": {
183
+ "domain_tags": {
184
+ "primary": { "domain": "react_frontend", "confidence": 0.49 },
185
+ "secondary": { "domain": "vue_frontend", "confidence": 0.47 },
186
+ "tertiary": { "domain": "typescript_node", "confidence": 0.40 }
187
+ },
188
+ "session_embedding": {
189
+ "model": "sentence-transformers/all-MiniLM-L6-v2",
190
+ "vector": [0.0123, -0.0456, "...384 floats"],
191
+ "source_text": "frontend\nI'll scaffold the project..."
192
+ }
193
+ }
194
+ }
195
+ ```
196
+
197
+ Confidence scores in the 0.25-0.55 range (cosine similarity), NOT 0.06-0.08 (keyword hit ratio).
@@ -0,0 +1,3 @@
1
+ {"envelope_id":"env_70dfa132-575b-48a3-becc-a2ea8a75aecc","session_id":"sess_54e35697-302f-45bc-bc3e-c4282b5f6034","chunk_sequence":0,"contributor_id":"23e6dd8c4a884836bf78cafda9fbe57d","attestation":{"session_hmac":"9f2d5a6369edcb2545181101574e61e02c65e939e253c068d398d2cbf8f20cd4","sequence":0,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"claude-opus-4-6","provider":"claude-code","agent_role":"fullstack","agent_id":"e18a8fa4","task_complexity":"medium","team_size":2,"session_quality":0,"groove_version":"0.27.113","leaf_context":null},"trajectory_log":[{"step":1,"type":"thought","timestamp":1777354900.276,"content":"Let me trace the exact URL the client is using.","token_count":12},{"step":2,"type":"action","timestamp":1777354901.244,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && grep -n 'EMBEDDING_SERVICE_URL\\|CENTRAL_COMMAND_URL' moe-training/shared/constants.js"},"content":"Using Bash","token_count":37},{"step":3,"type":"observation","timestamp":1777354901.808,"content":"41:export const CENTRAL_COMMAND_URL = process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai';\n42:export const EMBEDDING_SERVICE_URL = process.env.EMBEDDING_SERVICE_URL || `${CENTRAL_COMMAND_URL}/v1/embed`;\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":71,"is_error":false,"tool":"Bash","token_count":69},{"step":4,"type":"action","timestamp":1777354905.484,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && grep -n 'serviceUrl\\|EMBEDDING' moe-training/client/domain-tagger.js | head -10"},"content":"Using Bash","token_count":36},{"step":5,"type":"observation","timestamp":1777354905.528,"content":"3:import { EMBEDDING_SERVICE_URL } from '../shared/constants.js';\n176: this._serviceUrl = options.serviceUrl !== undefined ? options.serviceUrl : EMBEDDING_SERVICE_URL;\n208: if (this._serviceUrl) {\n210: const res = await fetch(this._serviceUrl, {\n361: const res = await fetch(this._serviceUrl, {\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":97,"is_error":false,"tool":"Bash","token_count":95},{"step":6,"type":"thought","timestamp":1777354911.293,"content":"The constant looks correct — `https://api.groovedev.ai/v1/embed`, no trailing slash. But let me check what the DomainTagger actually receives at runtime. The `TrajectoryCapture` creates the DomainTagger — let me see if it passes any options.","token_count":61},{"step":7,"type":"action","timestamp":1777354911.644,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && grep -n 'DomainTagger\\|domainTagger\\|_domainTagger' moe-training/client/trajectory-capture.js"},"content":"Using Bash","token_count":40},{"step":8,"type":"observation","timestamp":1777354911.695,"content":"11:import { DomainTagger } from './domain-tagger.js';\n48: this._domainTagger = new DomainTagger();\n49: await this._domainTagger.init();\n299: if (this._domainTagger) {\n304: const routingText = DomainTagger.buildRoutingText(role, firstPrompt, remainingThoughts);\n305: ctx.metadata.domain_tags = await this._domainTagger.tag(routingText);\n306: ctx.metadata.session_embedding = await this._domainTagger.embed(routingText);\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":129,"is_error":false,"tool":"Bash","token_count":127},{"step":9,"type":"thought","timestamp":1777354916.313,"content":"`new DomainTagger()` — no options passed, so it uses the default `EMBEDDING_SERVICE_URL`. Now let me check where the daemon actually imports and uses this. The `moe-training` module is consumed by the daemon.","token_count":52},{"step":10,"type":"action","timestamp":1777354916.852,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && grep -rn 'TrajectoryCapture\\|trajectory-capture\\|moe-training' packages/daemon/src/ | grep -v node_modules"},"content":"Using Bash","token_count":43},{"step":11,"type":"observation","timestamp":1777354916.946,"content":"packages/daemon/src/index.js:46:import { TrajectoryCapture, ConsentManager } from '../../../moe-training/client/index.js';\npackages/daemon/src/index.js:594: this._initTrajectoryCapture().catch(() => {});\npackages/daemon/src/index.js:680: async _initTrajectoryCapture() {\npackages/daemon/src/index.js:685: this.trajectoryCapture = new TrajectoryCapture({\npackages/daemon/src/api.js:18:import { ConsentManager } from '../../../moe-training/client/index.js';\npackages/daemon/src/api.js:4939: await daemon._initTrajectoryCapture();\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":155,"is_error":false,"tool":"Bash","token_count":153},{"step":12,"type":"action","timestamp":1777354920.727,"tool":"Read","arguments":{"file_path":"~/Desktop/groove-project/groove/packages/daemon/src/index.js","offset":678,"limit":25},"content":"Using Read","token_count":28},{"step":13,"type":"observation","timestamp":1777354920.728,"content":"678\t }\n679\t\n680\t async _initTrajectoryCapture() {\n681\t try {\n682\t if (!ConsentManager.isCaptureEnabled()) return;\n683\t const pkgPath = new URL('../package.json', import.meta.url);\n684\t const version = JSON.parse(readFileSync(pkgPath, 'utf8')).version;\n685\t this.trajectoryCapture = new TrajectoryCapture({\n686\t centralCommandUrl: process.env.GROOVE_CENTRAL_URL || 'https://api.groovedev.ai',\n687\t grooveVersion: version,\n688\t });\n689\t this.trajectoryCapture.init();\n690\t } catch (e) {\n691\t // Training capture is never critical\n692\t }\n693\t }\n694\t\n695\t _startGarbageCollector() {\n696\t // Run once on startup, then every 24 hours\n697\t this._gc();\n698\t this._gcInterval = setInterval(() => this._gc(), 24 * 60 * 60 * 1000);\n699\t\n700\t // Periodic state save — crash protection (every 30s)\n701\t this._stateSaveInterval = setInterval(async () => {\n702\t try { this.state.set('agents', this.registry.getAll()); await this.state.save(); } catch {}","truncated":false,"original_token_count":255,"is_error":false,"tool":"Read","token_count":255},{"step":14,"type":"thought","timestamp":1777354925.525,"content":"Line 689 — `this.trajectoryCapture.init()` — no `await`. That's not the bug here though. Let me look at what `TrajectoryCapture.init()` does with the DomainTagger.","token_count":41},{"step":15,"type":"action","timestamp":1777354926.407,"tool":"Read","arguments":{"file_path":"~/Desktop/groove-project/groove/moe-training/client/trajectory-capture.js","offset":38,"limit":16},"content":"Using Read","token_count":31},{"step":16,"type":"observation","timestamp":1777354926.407,"content":"38\t async init() {\n39\t if (!ConsentManager.isCaptureEnabled()) {\n40\t this._enabled = false;\n41\t return;\n42\t }\n43\t this._enabled = true;\n44\t this._scrubber = new PIIScrubber();\n45\t this._attestation = new SessionAttestation(this._centralCommandUrl);\n46\t this._transmissionQueue = new TransmissionQueue(this._centralCommandUrl);\n47\t this._transmissionQueue.start();\n48\t this._domainTagger = new DomainTagger();\n49\t await this._domainTagger.init();\n50\t this._offlineRetryTimer = setInterval(() => {\n51\t this._retryOfflineQueue();\n52\t }, OFFLINE_RETRY_INTERVAL_MS);\n53\t }","truncated":false,"original_token_count":155,"is_error":false,"tool":"Read","token_count":155},{"step":17,"type":"thought","timestamp":1777354935.764,"content":"Wait — line 689 in `index.js`: `this.trajectoryCapture.init()` — **no await**. That means `init()` runs as a fire-and-forget promise. The DomainTagger's `init()` calls `await this._domainTagger.init()` which does 41 HTTP requests. But the daemon doesn't await the TrajectoryCapture's init.\n\nThat's fine in theory — the DomainTagger should still eventually resolve. But let me check: if `init()` isn't awaited and an agent spawns before the DomainTagger finishes its 41 embed calls, the `_domainTagger` might be set but not yet `_ready`. Let me look at the timing.\n\nActually, that's not the issue either — `onAgentSpawn` doesn't check `_domainTagger.ready`. The domain tagging only happens in `_closeAgent`, by which point init would have long completed.\n\nLet me look at this from another angle — is the `EMBEDDING_SERVICE_URL` constant actually making it into the published npm package?","token_count":222},{"step":18,"type":"action","timestamp":1777354936.676,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && node -e \"\nimport { EMBEDDING_SERVICE_URL, CENTRAL_COMMAND_URL } from './moe-training/shared/constants.js';\nconsole.log('CENTRAL_COMMAND_URL:', CENTRAL_COMMAND_URL);\nconsole.log('EMBEDDING_SERVICE_URL:', EMBEDDING_SERVICE_URL);\n\""},"content":"Using Bash","token_count":74},{"step":19,"type":"observation","timestamp":1777354936.764,"content":"CENTRAL_COMMAND_URL: https://api.groovedev.ai\nEMBEDDING_SERVICE_URL: https://api.groovedev.ai/v1/embed\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":44,"is_error":false,"tool":"Bash","token_count":42},{"step":20,"type":"action","timestamp":1777354948.816,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && node -e \"\nimport { DomainTagger } from './moe-training/client/domain-tagger.js';\nconst t = new DomainTagger();\nconsole.log('serviceUrl:', t._serviceUrl);\nconsole.log('mode:', t._mode);\nconsole.log('ready:', t._ready);\n\""},"content":"Using Bash","token_count":73},{"step":21,"type":"observation","timestamp":1777354948.897,"content":"serviceUrl: https://api.groovedev.ai/v1/embed\nmode: null\nready: false\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":36,"is_error":false,"tool":"Bash","token_count":34},{"step":22,"type":"thought","timestamp":1777354952.865,"content":"URL is correct. Let me test the full init flow.","token_count":12},{"step":23,"type":"action","timestamp":1777354956.383,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && node -e \"\nimport { DomainTagger } from './moe-training/client/domain-tagger.js';\nconst t = new DomainTagger();\nconsole.log('serviceUrl:', t._serviceUrl);\nconsole.time('init');\ntry {\n await t.init();\n console.timeEnd('init');\n console.log('mode:', t.mode);\n console.log('ready:', t.ready);\n console.log('lastError:', t.lastError);\n if (t._centroids) {\n console.log('centroids:', Object.keys(t._centroids).length);\n }\n const result = await t.tag('Build a React frontend with TypeScript and Tailwind CSS');\n console.log('tag result:', JSON.stringify(result));\n const emb = await t.embed('Build a React frontend with TypeScript');\n console.log('embed:', emb ? emb.vector.length + 'd' : 'null');\n} catch (e) {\n console.error('ERROR:', e.message);\n}\n\" 2>&1","timeout":60000},"content":"Using Bash","token_count":217},{"step":24,"type":"observation","timestamp":1777354961.386,"content":"serviceUrl: https://api.groovedev.ai/v1/embed\ninit: 4.706s\nmode: http\nready: true\nlastError: null\ncentroids: 40\ntag result: {\"primary\":{\"domain\":\"react_frontend\",\"confidence\":0.685},\"secondary\":{\"domain\":\"typescript_node\",\"confidence\":0.4633},\"tertiary\":{\"domain\":\"vue_frontend\",\"confidence\":0.3264}}\nembed: 384d\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":97,"is_error":false,"tool":"Bash","token_count":94},{"step":25,"type":"thought","timestamp":1777354969.631,"content":"It works perfectly from this machine — `http` mode, 40 centroids, 0.685 confidence on React, 384d embedding. The code is correct.\n\nThe issue is that the **running daemon** was started before these changes were committed. The daemon process has the old code in memory where `DomainTagger` didn't import `EMBEDDING_SERVICE_URL` and defaulted `serviceUrl` to `null`.\n\nYou need to restart the Groove daemon to pick up the new code. The changes are in the files but the running Node process is still using the old version.","token_count":130},{"step":26,"type":"action","timestamp":1777354971.472,"tool":"Bash","arguments":{"command":"cd ~/Desktop/groove-project/groove && node -e \"\nimport { DomainTagger } from './moe-training/client/domain-tagger.js';\nconst t = new DomainTagger();\nconsole.log('Confirmed: serviceUrl =', t._serviceUrl);\nconsole.log('This proves the code is correct.');\nconsole.log('If live sessions show keyword mode, the running daemon has stale code in memory.');\nconsole.log('Fix: restart the daemon (groove stop && groove start)');\n\""},"content":"Using Bash","token_count":114},{"step":27,"type":"observation","timestamp":1777354971.549,"content":"Confirmed: serviceUrl = https://api.groovedev.ai/v1/embed\nThis proves the code is correct.\nIf live sessions show keyword mode, the running daemon has stale code in memory.\nFix: restart the daemon (groove stop && groove start)\nShell cwd was reset to ~/Desktop/groove-project/groove/training","truncated":false,"original_token_count":75,"is_error":false,"tool":"Bash","token_count":73},{"step":28,"type":"thought","timestamp":1777354976.411,"content":"Mystery solved. The code works — `http` mode, 40 centroids built in 4.7s, 0.685 confidence, 384d embeddings. The running daemon just needs a restart to pick up the new `EMBEDDING_SERVICE_URL` default. Run `groove stop && groove start` and the next session will have full embeddings.","token_count":71},{"step":29,"type":"resolution","timestamp":1777354976.457,"content":"Mystery solved. The code works — `http` mode, 40 centroids built in 4.7s, 0.685 confidence, 384d embeddings. The running daemon just needs a restart to pick up the new `EMBEDDING_SERVICE_URL` default. Run `groove stop && groove start` and the next session will have full embeddings.","token_count":71}]}
2
+ {"envelope_id":"env_8c59a800-92c2-486e-aab2-d251958883da","session_id":"sess_54e35697-302f-45bc-bc3e-c4282b5f6034","type":"SESSION_CLOSE","attestation":{"session_hmac":"3fd6f3891108e43eb7482a0361fc5789ecfb9fdf40f637a63edb01e154c6f74f","sequence":1,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"metadata":{"model_engine":"claude-opus-4-6","provider":"claude-code","agent_role":"fullstack","agent_id":"e18a8fa4","task_complexity":"medium","team_size":2,"session_quality":70,"groove_version":"0.27.113","leaf_context":null,"domain_tags":{"primary":{"domain":"linux_admin","confidence":0.0769},"secondary":{"domain":"nlp_llm","confidence":0.0769},"tertiary":{"domain":"react_frontend","confidence":0.0667}},"session_embedding":null},"outcome":{"status":"SUCCESS","session_quality":70,"quality_tier":"TIER_A","quality_tier_reason":"high_quality_no_errors","user_interventions":0,"total_steps":29,"total_chunks":1,"total_tokens":2462,"duration_seconds":86,"files_modified":0,"errors_encountered":0,"errors_recovered":0,"coordination_events":0,"training_eligible":true,"training_exclusion_reason":null}}
3
+ {"envelope_id":"env_eafaf868-6b85-46c8-b687-43a576aedc50","session_id":"sess_54e35697-302f-45bc-bc3e-c4282b5f6034","type":"USER_FEEDBACK","attestation":{"session_hmac":"a60cc84ce26d8714749bbaedd3d51a8fb153d008e3cb396d3413740d6970454c","sequence":2,"app_version_hash":"4a527565d85cc601d3c47c376e4f361d5af987de58f60cf71dbc263a0de91247"},"feedback":{"signal":"accepted","timestamp":1777354977.009,"context":"session completed successfully with no user interventions","target_step":29,"revision_rounds":0,"delta_summary":null}}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.113",
3
+ "version": "0.27.115",
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,9 +1,17 @@
1
1
  // GROOVE CLI — team commands
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
+ import { createInterface } from 'readline';
4
5
  import chalk from 'chalk';
5
6
  import { apiCall } from '../client.js';
6
7
 
8
+ function confirm(prompt) {
9
+ return new Promise((resolve) => {
10
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
11
+ rl.question(prompt, (answer) => { rl.close(); resolve(answer.trim().toLowerCase() === 'y'); });
12
+ });
13
+ }
14
+
7
15
  export async function teamCreate(name) {
8
16
  try {
9
17
  const team = await apiCall('POST', '/api/teams', { name });
@@ -37,9 +45,43 @@ export async function teamList() {
37
45
  }
38
46
 
39
47
  export async function teamDelete(id) {
48
+ const ok = await confirm(` This will archive the team directory. Continue? [y/N] `);
49
+ if (!ok) {
50
+ console.log(chalk.dim(' Cancelled.'));
51
+ return;
52
+ }
40
53
  try {
41
54
  await apiCall('DELETE', `/api/teams/${encodeURIComponent(id)}`);
42
- console.log(chalk.green(` Deleted team "${id}"`));
55
+ console.log(chalk.green(` Archived team "${id}"`) + chalk.dim(' — restore with `groove team restore <id>`'));
56
+ } catch (err) {
57
+ console.error(chalk.red(' Failed:'), err.message);
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ export async function teamArchived() {
63
+ try {
64
+ const { archived } = await apiCall('GET', '/api/teams/archived');
65
+ if (archived.length === 0) {
66
+ console.log(chalk.dim(' No archived teams.'));
67
+ return;
68
+ }
69
+ console.log(chalk.bold(`\n Archived Teams (${archived.length})\n`));
70
+ for (const t of archived) {
71
+ const date = t.deletedAt ? new Date(t.deletedAt).toLocaleDateString() : 'unknown';
72
+ console.log(` ${chalk.bold(t.originalName || t.id)} — archive-id: ${t.id} — deleted ${date} — ${t.agentCount} agent(s)`);
73
+ }
74
+ console.log('');
75
+ } catch {
76
+ console.error(chalk.red(' Cannot connect to daemon.'));
77
+ process.exit(1);
78
+ }
79
+ }
80
+
81
+ export async function teamRestore(id) {
82
+ try {
83
+ const team = await apiCall('POST', `/api/teams/archived/${encodeURIComponent(id)}/restore`);
84
+ console.log(chalk.green(` Restored team "${team.name}"`) + ` (new id: ${team.id})`);
43
85
  } catch (err) {
44
86
  console.error(chalk.red(' Failed:'), err.message);
45
87
  process.exit(1);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.113",
3
+ "version": "0.27.115",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1100,6 +1100,30 @@ export function createApi(app, daemon) {
1100
1100
  }
1101
1101
  });
1102
1102
 
1103
+ app.get('/api/teams/archived', (req, res) => {
1104
+ res.json({ archived: daemon.teams.listArchived() });
1105
+ });
1106
+
1107
+ app.post('/api/teams/archived/:id/restore', (req, res) => {
1108
+ try {
1109
+ const team = daemon.teams.restore(req.params.id);
1110
+ daemon.audit.log('team.restore', { archivedId: req.params.id, newId: team.id, name: team.name });
1111
+ res.json(team);
1112
+ } catch (err) {
1113
+ res.status(400).json({ error: err.message });
1114
+ }
1115
+ });
1116
+
1117
+ app.delete('/api/teams/archived/:id', (req, res) => {
1118
+ try {
1119
+ daemon.teams.purge(req.params.id);
1120
+ daemon.audit.log('team.purge', { archivedId: req.params.id });
1121
+ res.json({ ok: true });
1122
+ } catch (err) {
1123
+ res.status(400).json({ error: err.message });
1124
+ }
1125
+ });
1126
+
1103
1127
  app.patch('/api/teams/:id', (req, res) => {
1104
1128
  try {
1105
1129
  if (req.body.name) daemon.teams.rename(req.params.id, req.body.name);
@@ -8,6 +8,7 @@ export class FileWatcher {
8
8
  constructor(daemon) {
9
9
  this.daemon = daemon;
10
10
  this.watchers = new Map(); // relPath → { watcher, timer }
11
+ this.dirWatchers = new Map(); // relPath → { watcher, timer }
11
12
  }
12
13
 
13
14
  watch(relPath) {
@@ -51,9 +52,53 @@ export class FileWatcher {
51
52
  this.watchers.delete(relPath);
52
53
  }
53
54
 
55
+ watchDir(relPath) {
56
+ if (typeof relPath !== 'string') return;
57
+ if (relPath && relPath.includes('..')) return;
58
+ if (this.dirWatchers.has(relPath)) return;
59
+
60
+ const fullPath = relPath ? resolve(this.daemon.projectDir, relPath) : this.daemon.projectDir;
61
+
62
+ try {
63
+ const watcher = watch(fullPath, () => {
64
+ const entry = this.dirWatchers.get(relPath);
65
+ if (!entry) return;
66
+
67
+ if (entry.timer) clearTimeout(entry.timer);
68
+ entry.timer = setTimeout(() => {
69
+ this.daemon.broadcast({
70
+ type: 'file:tree-changed',
71
+ path: relPath,
72
+ timestamp: Date.now(),
73
+ });
74
+ }, 300);
75
+ });
76
+
77
+ watcher.on('error', () => {
78
+ this.unwatchDir(relPath);
79
+ });
80
+
81
+ this.dirWatchers.set(relPath, { watcher, timer: null });
82
+ } catch {
83
+ // Directory doesn't exist or not watchable — ignore
84
+ }
85
+ }
86
+
87
+ unwatchDir(relPath) {
88
+ const entry = this.dirWatchers.get(relPath);
89
+ if (!entry) return;
90
+
91
+ if (entry.timer) clearTimeout(entry.timer);
92
+ try { entry.watcher.close(); } catch { /* already closed */ }
93
+ this.dirWatchers.delete(relPath);
94
+ }
95
+
54
96
  unwatchAll() {
55
97
  for (const [relPath] of this.watchers) {
56
98
  this.unwatch(relPath);
57
99
  }
100
+ for (const [relPath] of this.dirWatchers) {
101
+ this.unwatchDir(relPath);
102
+ }
58
103
  }
59
104
  }
@@ -325,8 +325,9 @@ export class Daemon {
325
325
  data: enrichAgents(this.registry.getAll()),
326
326
  }));
327
327
 
328
- // Track which files this client is watching (for cleanup on disconnect)
328
+ // Track which files/dirs this client is watching (for cleanup on disconnect)
329
329
  const watchedFiles = new Set();
330
+ const watchedDirs = new Set();
330
331
 
331
332
  ws.on('message', (raw) => {
332
333
  try {
@@ -335,7 +336,7 @@ export class Daemon {
335
336
  // Validate message type against whitelist
336
337
  const VALID_WS_TYPES = new Set([
337
338
  'terminal:spawn', 'terminal:resize', 'terminal:input', 'terminal:close', 'terminal:kill', 'terminal:rename',
338
- 'editor:watch', 'editor:unwatch', 'editor:save',
339
+ 'editor:watch', 'editor:unwatch', 'editor:save', 'editor:watchdir', 'editor:unwatchdir',
339
340
  'ping'
340
341
  ]);
341
342
  if (!msg || typeof msg !== 'object' || !VALID_WS_TYPES.has(msg.type)) return;
@@ -351,6 +352,14 @@ export class Daemon {
351
352
  case 'editor:unwatch':
352
353
  if (msg.path) { this.fileWatcher.unwatch(msg.path); watchedFiles.delete(msg.path); }
353
354
  break;
355
+ case 'editor:watchdir':
356
+ if (typeof msg.path === 'string' && !msg.path.includes('..')) {
357
+ this.fileWatcher.watchDir(msg.path); watchedDirs.add(msg.path);
358
+ }
359
+ break;
360
+ case 'editor:unwatchdir':
361
+ if (typeof msg.path === 'string') { this.fileWatcher.unwatchDir(msg.path); watchedDirs.delete(msg.path); }
362
+ break;
354
363
  // Terminal
355
364
  case 'terminal:spawn': {
356
365
  if (msg.cwd !== undefined && (typeof msg.cwd !== 'string' || msg.cwd.includes('..'))) break;
@@ -389,6 +398,9 @@ export class Daemon {
389
398
  for (const path of watchedFiles) {
390
399
  this.fileWatcher.unwatch(path);
391
400
  }
401
+ for (const path of watchedDirs) {
402
+ this.fileWatcher.unwatchDir(path);
403
+ }
392
404
  this.terminalManager.cleanupClient(ws);
393
405
  });
394
406
  });
@@ -1,8 +1,8 @@
1
1
  // GROOVE — Teams (Live Agent Groups)
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, rmSync } from 'fs';
5
- import { resolve } from 'path';
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, rmSync, readdirSync, cpSync } from 'fs';
5
+ import { resolve, basename } from 'path';
6
6
  import { randomUUID } from 'crypto';
7
7
  import { validateTeamName } from './validate.js';
8
8
 
@@ -162,17 +162,39 @@ export class Teams {
162
162
  this.daemon.registry.remove(agent.id);
163
163
  }
164
164
 
165
- // Remove the team's working directory refuse to nuke the project root
166
- // (legacy default teams that were never migrated point there).
165
+ // Archive the team's working directory instead of deleting it
167
166
  if (
168
167
  team.workingDir &&
169
168
  team.workingDir !== this.daemon.projectDir &&
170
169
  existsSync(team.workingDir)
171
170
  ) {
172
171
  try {
173
- rmSync(team.workingDir, { recursive: true, force: true });
172
+ const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
173
+ mkdirSync(archiveDir, { recursive: true });
174
+ const slug = basename(team.workingDir);
175
+ const archiveName = `${slug}-${Date.now()}`;
176
+ const archivePath = resolve(archiveDir, archiveName);
177
+
178
+ try {
179
+ renameSync(team.workingDir, archivePath);
180
+ } catch (err) {
181
+ if (err.code === 'EXDEV') {
182
+ cpSync(team.workingDir, archivePath, { recursive: true });
183
+ rmSync(team.workingDir, { recursive: true, force: true });
184
+ } else {
185
+ throw err;
186
+ }
187
+ }
188
+
189
+ const metadata = {
190
+ originalName: team.name,
191
+ originalId: team.id,
192
+ deletedAt: new Date().toISOString(),
193
+ agentCount: agents.length,
194
+ };
195
+ writeFileSync(resolve(archivePath, 'metadata.json'), JSON.stringify(metadata, null, 2));
174
196
  } catch (err) {
175
- console.log(`[Groove:Teams] Failed to remove directory: ${err.message}`);
197
+ console.log(`[Groove:Teams] Failed to archive directory: ${err.message}`);
176
198
  }
177
199
  }
178
200
 
@@ -193,6 +215,78 @@ export class Teams {
193
215
  return true;
194
216
  }
195
217
 
218
+ listArchived() {
219
+ const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
220
+ if (!existsSync(archiveDir)) return [];
221
+ const entries = readdirSync(archiveDir, { withFileTypes: true });
222
+ const result = [];
223
+ for (const entry of entries) {
224
+ if (!entry.isDirectory()) continue;
225
+ const metaPath = resolve(archiveDir, entry.name, 'metadata.json');
226
+ try {
227
+ const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
228
+ result.push({ id: entry.name, ...meta });
229
+ } catch {
230
+ result.push({ id: entry.name, originalName: entry.name, deletedAt: null, agentCount: 0 });
231
+ }
232
+ }
233
+ return result;
234
+ }
235
+
236
+ restore(archivedId) {
237
+ const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
238
+ const archivePath = resolve(archiveDir, archivedId);
239
+ if (!existsSync(archivePath)) throw new Error('Archived team not found');
240
+
241
+ let meta = {};
242
+ const metaPath = resolve(archivePath, 'metadata.json');
243
+ try { meta = JSON.parse(readFileSync(metaPath, 'utf8')); } catch { /* use defaults */ }
244
+
245
+ const name = meta.originalName || archivedId;
246
+ const dirName = slugify(name);
247
+ let workingDir = resolve(this.daemon.projectDir, dirName);
248
+
249
+ if (existsSync(workingDir)) {
250
+ workingDir = resolve(this.daemon.projectDir, `${dirName}-${Date.now()}`);
251
+ }
252
+
253
+ try {
254
+ renameSync(archivePath, workingDir);
255
+ } catch (err) {
256
+ if (err.code === 'EXDEV') {
257
+ cpSync(archivePath, workingDir, { recursive: true });
258
+ rmSync(archivePath, { recursive: true, force: true });
259
+ } else {
260
+ throw err;
261
+ }
262
+ }
263
+
264
+ // Remove the metadata file from the restored directory
265
+ const restoredMetaPath = resolve(workingDir, 'metadata.json');
266
+ try { rmSync(restoredMetaPath); } catch { /* may not exist */ }
267
+
268
+ const id = randomUUID().slice(0, 8);
269
+ const team = {
270
+ id,
271
+ name,
272
+ isDefault: false,
273
+ workingDir,
274
+ createdAt: new Date().toISOString(),
275
+ };
276
+ this.teams.set(id, team);
277
+ this._save();
278
+ this.daemon.broadcast({ type: 'team:created', team });
279
+ return team;
280
+ }
281
+
282
+ purge(archivedId) {
283
+ const archiveDir = resolve(this.daemon.grooveDir, 'archived-teams');
284
+ const archivePath = resolve(archiveDir, archivedId);
285
+ if (!existsSync(archivePath)) throw new Error('Archived team not found');
286
+ rmSync(archivePath, { recursive: true, force: true });
287
+ return true;
288
+ }
289
+
196
290
  // Migrate old agents (teamName but no teamId) to default team
197
291
  migrateAgents() {
198
292
  const defaultTeam = this.getDefault();