groove-dev 0.27.87 → 0.27.88

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 (53) hide show
  1. package/CLAUDE.md +3 -2
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +115 -7
  5. package/node_modules/@groove-dev/daemon/src/conversations.js +29 -3
  6. package/node_modules/@groove-dev/daemon/src/providers/codex.js +28 -10
  7. package/node_modules/@groove-dev/daemon/src/registry.js +30 -0
  8. package/node_modules/@groove-dev/daemon/src/validate.js +23 -0
  9. package/node_modules/@groove-dev/gui/dist/assets/index-BSqk8cbI.css +1 -0
  10. package/node_modules/@groove-dev/gui/dist/assets/index-B_igwWvq.js +8642 -0
  11. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  12. package/node_modules/@groove-dev/gui/package.json +1 -1
  13. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +254 -0
  14. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +177 -0
  15. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +148 -0
  16. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +377 -0
  17. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +117 -40
  18. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +10 -13
  19. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +26 -1
  20. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +14 -14
  21. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +5 -0
  22. package/node_modules/@groove-dev/gui/src/stores/groove.js +132 -1
  23. package/node_modules/@groove-dev/gui/src/views/agents.jsx +22 -3
  24. package/package.json +1 -1
  25. package/packages/cli/package.json +1 -1
  26. package/packages/daemon/package.json +1 -1
  27. package/packages/daemon/src/api.js +115 -7
  28. package/packages/daemon/src/conversations.js +29 -3
  29. package/packages/daemon/src/providers/codex.js +28 -10
  30. package/packages/daemon/src/registry.js +30 -0
  31. package/packages/daemon/src/validate.js +23 -0
  32. package/packages/gui/dist/assets/index-BSqk8cbI.css +1 -0
  33. package/packages/gui/dist/assets/index-B_igwWvq.js +8642 -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/agent-file-tree.jsx +254 -0
  37. package/packages/gui/src/components/agents/code-review.jsx +177 -0
  38. package/packages/gui/src/components/agents/diff-viewer.jsx +148 -0
  39. package/packages/gui/src/components/agents/workspace-mode.jsx +377 -0
  40. package/packages/gui/src/components/chat/chat-input.jsx +117 -40
  41. package/packages/gui/src/components/chat/chat-messages.jsx +10 -13
  42. package/packages/gui/src/components/chat/chat-view.jsx +26 -1
  43. package/packages/gui/src/components/chat/conversation-list.jsx +14 -14
  44. package/packages/gui/src/components/chat/model-picker.jsx +5 -0
  45. package/packages/gui/src/stores/groove.js +132 -1
  46. package/packages/gui/src/views/agents.jsx +22 -3
  47. package/test/doomsday-clock/index.html +55 -0
  48. package/test/doomsday-clock/script.js +66 -0
  49. package/test/doomsday-clock/style.css +315 -0
  50. package/node_modules/@groove-dev/gui/dist/assets/index-BCQY8ojz.css +0 -1
  51. package/node_modules/@groove-dev/gui/dist/assets/index-C5e7KVGN.js +0 -8637
  52. package/packages/gui/dist/assets/index-BCQY8ojz.css +0 -1
  53. package/packages/gui/dist/assets/index-C5e7KVGN.js +0 -8637
package/CLAUDE.md CHANGED
@@ -266,10 +266,11 @@ Audit-driven release. Multi-agent orchestration system with 7 coordination layer
266
266
 
267
267
  <!-- GROOVE:START -->
268
268
  ## GROOVE Orchestration (auto-injected)
269
- Active agents: 1
269
+ Active agents: 2
270
270
  | Name | Role | Scope |
271
271
  |------|------|-------|
272
- | backend-9 | backend | packages/cli/src/commands/connect.js, packages/cli/src/commands/disconnect.js, packages/cli/src/commands/remotes.js, packages/cli/bin/groove.js |
272
+ | fullstack-23 | fullstack | - |
273
+ | fullstack-12 | fullstack | - |
273
274
  See AGENTS_REGISTRY.md for full agent state.
274
275
  **Memory policy:** GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.
275
276
  <!-- GROOVE:END -->
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.87",
3
+ "version": "0.27.88",
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.87",
3
+ "version": "0.27.88",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -16,7 +16,7 @@ import { OllamaProvider } from './providers/ollama.js';
16
16
  import { ClaudeCodeProvider } from './providers/claude-code.js';
17
17
  import { supportsSignalFlag, compareSemver, parseSemver } from './providers/groove-network.js';
18
18
  import { ConsentManager } from '../../../moe-training/client/index.js';
19
- import { validateAgentConfig } from './validate.js';
19
+ import { validateAgentConfig, validateReasoningEffort, validateVerbosity } from './validate.js';
20
20
  import { ROLE_INTEGRATIONS, wrapWithRoleReminder } from './process.js';
21
21
 
22
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -72,6 +72,9 @@ async function _executeApprovalRetry(daemon, approval) {
72
72
  }
73
73
  }
74
74
 
75
+ const FILE_READ_TOOLS = new Set(['Read', 'read_file']);
76
+ const FILE_WRITE_TOOLS = new Set(['Write', 'Edit', 'write_file', 'edit_file', 'create_file']);
77
+
75
78
  export function createApi(app, daemon) {
76
79
  _daemon = daemon;
77
80
 
@@ -325,6 +328,14 @@ export function createApi(app, daemon) {
325
328
  }
326
329
  }
327
330
 
331
+ // Track file operations for the files-touched API
332
+ if (targets.length > 0) {
333
+ const op = FILE_WRITE_TOOLS.has(toolName) ? 'write' : FILE_READ_TOOLS.has(toolName) ? 'read' : null;
334
+ if (op) {
335
+ for (const t of targets) daemon.registry.trackFileOp(agentId, t, op);
336
+ }
337
+ }
338
+
328
339
  daemon.audit.log('knock.allowed', { agentId, toolName, targets });
329
340
  res.json({ allow: true });
330
341
  });
@@ -1100,14 +1111,19 @@ export function createApi(app, daemon) {
1100
1111
 
1101
1112
  app.post('/api/conversations', async (req, res) => {
1102
1113
  try {
1103
- const { provider, model, title, mode } = req.body;
1114
+ const { provider, model, title, mode, reasoning_effort, verbosity } = req.body;
1104
1115
  if (!provider || typeof provider !== 'string') {
1105
1116
  return res.status(400).json({ error: 'provider is required' });
1106
1117
  }
1107
1118
  if (mode && mode !== 'api' && mode !== 'agent') {
1108
1119
  return res.status(400).json({ error: 'mode must be "api" or "agent"' });
1109
1120
  }
1110
- const conversation = await daemon.conversations.create(provider, model, title, mode || 'api');
1121
+ const validatedEffort = validateReasoningEffort(reasoning_effort);
1122
+ const validatedVerbosity = validateVerbosity(verbosity);
1123
+ const conversation = await daemon.conversations.create(provider, model, title, mode || 'api', {
1124
+ reasoningEffort: validatedEffort,
1125
+ verbosity: validatedVerbosity,
1126
+ });
1111
1127
  daemon.audit.log('conversation.create', { id: conversation.id, provider, model, mode: conversation.mode });
1112
1128
  res.status(201).json(conversation);
1113
1129
  } catch (err) {
@@ -1139,6 +1155,11 @@ export function createApi(app, daemon) {
1139
1155
  }
1140
1156
  await daemon.conversations.setMode(req.params.id, req.body.mode);
1141
1157
  }
1158
+ if (req.body.reasoning_effort !== undefined || req.body.verbosity !== undefined) {
1159
+ const validatedEffort = req.body.reasoning_effort !== undefined ? validateReasoningEffort(req.body.reasoning_effort) : undefined;
1160
+ const validatedVerbosity = req.body.verbosity !== undefined ? validateVerbosity(req.body.verbosity) : undefined;
1161
+ daemon.conversations.updateReasoningSettings(req.params.id, validatedEffort, validatedVerbosity);
1162
+ }
1142
1163
  daemon.audit.log('conversation.update', { id: req.params.id, provider: req.body.provider, model: req.body.model, mode: req.body.mode });
1143
1164
  res.json(daemon.conversations.get(req.params.id));
1144
1165
  } catch (err) {
@@ -1160,10 +1181,13 @@ export function createApi(app, daemon) {
1160
1181
 
1161
1182
  app.post('/api/conversations/:id/message', async (req, res) => {
1162
1183
  try {
1163
- const { message, history } = req.body;
1184
+ const { message, history, reasoning_effort, verbosity } = req.body;
1164
1185
  if (!message || typeof message !== 'string' || !message.trim()) {
1165
1186
  return res.status(400).json({ error: 'message is required' });
1166
1187
  }
1188
+ const validatedEffort = validateReasoningEffort(reasoning_effort);
1189
+ const validatedVerbosity = validateVerbosity(verbosity);
1190
+
1167
1191
  const conv = daemon.conversations.get(req.params.id);
1168
1192
  if (!conv) return res.status(404).json({ error: 'Conversation not found' });
1169
1193
 
@@ -1172,7 +1196,10 @@ export function createApi(app, daemon) {
1172
1196
 
1173
1197
  // API mode — lightweight headless streaming, no agent spawned
1174
1198
  if (conv.mode === 'api' || !conv.agentId) {
1175
- await daemon.conversations.sendMessage(req.params.id, message.trim(), history || []);
1199
+ await daemon.conversations.sendMessage(req.params.id, message.trim(), history || [], {
1200
+ reasoningEffort: validatedEffort,
1201
+ verbosity: validatedVerbosity,
1202
+ });
1176
1203
  daemon.audit.log('conversation.message', { id: req.params.id, mode: 'api' });
1177
1204
  return res.json({ status: 'streaming', mode: 'api' });
1178
1205
  }
@@ -3064,6 +3091,87 @@ Keep responses concise. Help them think, don't lecture them about the system the
3064
3091
  });
3065
3092
  });
3066
3093
 
3094
+ // Files touched by an agent during its session
3095
+ app.get('/api/agents/:id/files-touched', (req, res) => {
3096
+ const agent = daemon.registry.get(req.params.id);
3097
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
3098
+ const files = daemon.registry.getFilesTouched(req.params.id);
3099
+ res.json({ files, total: files.length });
3100
+ });
3101
+
3102
+ // Git diff — structured diff for a file, an agent's touched files, or all uncommitted changes
3103
+ app.get('/api/files/git-diff', (req, res) => {
3104
+ const rootDir = getEditorRoot();
3105
+ if (!rootDir) return res.status(400).json({ error: 'Editor root not set' });
3106
+
3107
+ let paths = [];
3108
+
3109
+ if (req.query.path) {
3110
+ const result = validateFilePath(req.query.path, rootDir);
3111
+ if (result.error) return res.status(400).json({ error: result.error });
3112
+ paths = [req.query.path];
3113
+ } else if (req.query.agentId) {
3114
+ const agent = daemon.registry.get(req.query.agentId);
3115
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
3116
+ paths = daemon.registry.getFilesTouched(req.query.agentId).map(f => f.path);
3117
+ if (paths.length === 0) return res.json({ diffs: [] });
3118
+ // Validate each path
3119
+ for (const p of paths) {
3120
+ if (p.startsWith('/') || p.includes('..') || p.includes('\0')) {
3121
+ return res.status(400).json({ error: 'Invalid path in agent files' });
3122
+ }
3123
+ }
3124
+ }
3125
+
3126
+ const args = ['diff'];
3127
+ const cachedArgs = ['diff', '--cached'];
3128
+ if (paths.length > 0) {
3129
+ args.push('--', ...paths);
3130
+ cachedArgs.push('--', ...paths);
3131
+ }
3132
+
3133
+ try {
3134
+ const unstaged = execFileSync('git', args, { cwd: rootDir, timeout: 15000, maxBuffer: 10 * 1024 * 1024 }).toString();
3135
+ const staged = execFileSync('git', cachedArgs, { cwd: rootDir, timeout: 15000, maxBuffer: 10 * 1024 * 1024 }).toString();
3136
+ const combined = (staged + '\n' + unstaged).trim();
3137
+ const diffs = parseDiffOutput(combined);
3138
+ res.json({ diffs });
3139
+ } catch (err) {
3140
+ if (err.status !== undefined) {
3141
+ return res.json({ diffs: [] });
3142
+ }
3143
+ res.status(500).json({ error: 'Failed to compute diff' });
3144
+ }
3145
+ });
3146
+
3147
+ function parseDiffOutput(raw) {
3148
+ if (!raw) return [];
3149
+ const fileDiffs = raw.split(/^diff --git /m).filter(Boolean);
3150
+ return fileDiffs.map(chunk => {
3151
+ const lines = chunk.split('\n');
3152
+ const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
3153
+ const filePath = headerMatch ? headerMatch[2] : 'unknown';
3154
+ let status = 'modified';
3155
+ if (lines.some(l => l.startsWith('new file'))) status = 'added';
3156
+ else if (lines.some(l => l.startsWith('deleted file'))) status = 'deleted';
3157
+ let additions = 0, deletions = 0;
3158
+ const hunks = [];
3159
+ let currentHunk = null;
3160
+ for (const line of lines) {
3161
+ if (line.startsWith('@@')) {
3162
+ if (currentHunk) hunks.push(currentHunk);
3163
+ currentHunk = { header: line, lines: [] };
3164
+ } else if (currentHunk) {
3165
+ currentHunk.lines.push(line);
3166
+ if (line.startsWith('+') && !line.startsWith('+++')) additions++;
3167
+ else if (line.startsWith('-') && !line.startsWith('---')) deletions++;
3168
+ }
3169
+ }
3170
+ if (currentHunk) hunks.push(currentHunk);
3171
+ return { path: filePath, status, hunks, additions, deletions, content: 'diff --git ' + chunk };
3172
+ });
3173
+ }
3174
+
3067
3175
  // File search — fuzzy filename matching for quick-open (Ctrl+P)
3068
3176
  app.get('/api/files/search', (req, res) => {
3069
3177
  const query = req.query.q;
@@ -4172,10 +4280,10 @@ Keep responses concise. Help them think, don't lecture them about the system the
4172
4280
 
4173
4281
  app.post('/api/tunnels', (req, res) => {
4174
4282
  try {
4175
- const { name, host, user, port, sshKeyPath, autoStart, autoConnect } = req.body;
4283
+ const { name, host, user, port, sshKeyPath, autoStart, autoConnect, projectDir } = req.body;
4176
4284
  if (!name || typeof name !== 'string') return res.status(400).json({ error: 'name is required (string)' });
4177
4285
  if (!host || typeof host !== 'string') return res.status(400).json({ error: 'host is required (string)' });
4178
- const result = daemon.tunnelManager.save({ name, host, user, port, sshKeyPath, autoStart, autoConnect });
4286
+ const result = daemon.tunnelManager.save({ name, host, user, port, sshKeyPath, autoStart, autoConnect, projectDir });
4179
4287
  res.json(result);
4180
4288
  } catch (err) {
4181
4289
  res.status(400).json({ error: err.message });
@@ -55,7 +55,7 @@ export class ConversationManager {
55
55
  return null;
56
56
  }
57
57
 
58
- async create(provider, model, title, mode = 'api') {
58
+ async create(provider, model, title, mode = 'api', options = {}) {
59
59
  if (!provider && this.daemon.config?.defaultChatProvider) {
60
60
  provider = this.daemon.config.defaultChatProvider;
61
61
  }
@@ -90,6 +90,9 @@ export class ConversationManager {
90
90
  provider,
91
91
  model: model || null,
92
92
  mode: mode === 'agent' ? 'agent' : 'api',
93
+ reasoningEffort: options.reasoningEffort || null,
94
+ verbosity: options.verbosity || null,
95
+ previousResponseId: null,
93
96
  createdAt: now,
94
97
  updatedAt: now,
95
98
  pinned: false,
@@ -218,6 +221,17 @@ export class ConversationManager {
218
221
  return conv;
219
222
  }
220
223
 
224
+ updateReasoningSettings(id, reasoningEffort, verbosity) {
225
+ const conv = this.conversations.get(id);
226
+ if (!conv) throw new Error('Conversation not found');
227
+ if (reasoningEffort !== undefined) conv.reasoningEffort = reasoningEffort || null;
228
+ if (verbosity !== undefined) conv.verbosity = verbosity || null;
229
+ conv.updatedAt = new Date().toISOString();
230
+ this._save();
231
+ this.daemon.broadcast({ type: 'conversation:updated', data: conv });
232
+ return conv;
233
+ }
234
+
221
235
  async setMode(id, mode) {
222
236
  const conv = this.conversations.get(id);
223
237
  if (!conv) throw new Error('Conversation not found');
@@ -302,7 +316,7 @@ export class ConversationManager {
302
316
  } catch { return null; }
303
317
  }
304
318
 
305
- async sendMessage(id, message, history) {
319
+ async sendMessage(id, message, history, { reasoningEffort, verbosity } = {}) {
306
320
  const conv = this.conversations.get(id);
307
321
  if (!conv) throw new Error('Conversation not found');
308
322
  if (conv.mode !== 'api') throw new Error('sendMessage only works in API mode');
@@ -331,6 +345,9 @@ export class ConversationManager {
331
345
 
332
346
  const apiKey = this._getApiKey(providerName);
333
347
 
348
+ const effectiveReasoningEffort = reasoningEffort || conv.reasoningEffort || null;
349
+ const effectiveVerbosity = verbosity || conv.verbosity || null;
350
+
334
351
  // Try direct API streaming first (sub-second latency)
335
352
  const controller = provider.streamChat(
336
353
  messages, modelId, apiKey,
@@ -340,7 +357,11 @@ export class ConversationManager {
340
357
  data: { conversationId: id, text },
341
358
  });
342
359
  },
343
- () => {
360
+ (result) => {
361
+ if (result?.responseId) {
362
+ conv.previousResponseId = result.responseId;
363
+ this._save();
364
+ }
344
365
  this._getStreamingProcesses().delete(id);
345
366
  this.daemon.broadcast({
346
367
  type: 'conversation:complete',
@@ -354,6 +375,11 @@ export class ConversationManager {
354
375
  data: { conversationId: id, error: err.message },
355
376
  });
356
377
  },
378
+ {
379
+ reasoningEffort: effectiveReasoningEffort,
380
+ verbosity: effectiveVerbosity,
381
+ previousResponseId: conv.previousResponseId,
382
+ },
357
383
  );
358
384
 
359
385
  if (controller) {
@@ -153,22 +153,31 @@ export class CodexProvider extends Provider {
153
153
  return (inputTokens / 1000) * model.pricing.input + (outputTokens / 1000) * model.pricing.output;
154
154
  }
155
155
 
156
- streamChat(messages, model, apiKey, onChunk, onDone, onError) {
156
+ streamChat(messages, model, apiKey, onChunk, onDone, onError, { reasoningEffort, verbosity, previousResponseId } = {}) {
157
157
  if (!apiKey) return null;
158
158
  const controller = new AbortController();
159
159
  let finished = false;
160
- const finish = () => { if (!finished) { finished = true; onDone(); } };
161
- fetch('https://api.openai.com/v1/chat/completions', {
160
+ let responseId = null;
161
+ const finish = () => { if (!finished) { finished = true; onDone({ responseId }); } };
162
+
163
+ const effort = reasoningEffort || 'medium';
164
+ const verb = verbosity || 'medium';
165
+ const body = {
166
+ model: model || 'gpt-5.5',
167
+ input: previousResponseId ? [messages[messages.length - 1]] : messages,
168
+ stream: true,
169
+ reasoning: { effort },
170
+ text: { format: { type: 'text' }, verbosity: verb },
171
+ };
172
+ if (previousResponseId) body.previous_response_id = previousResponseId;
173
+
174
+ fetch('https://api.openai.com/v1/responses', {
162
175
  method: 'POST',
163
176
  headers: {
164
177
  'Authorization': `Bearer ${apiKey}`,
165
178
  'Content-Type': 'application/json',
166
179
  },
167
- body: JSON.stringify({
168
- model: model || 'gpt-5.4-mini',
169
- messages,
170
- stream: true,
171
- }),
180
+ body: JSON.stringify(body),
172
181
  signal: controller.signal,
173
182
  }).then((res) => {
174
183
  if (!res.ok) {
@@ -176,8 +185,11 @@ export class CodexProvider extends Provider {
176
185
  }
177
186
  return parseSSEStream(res, (event) => {
178
187
  if (event.done) { finish(); return; }
179
- const content = event.choices?.[0]?.delta?.content;
180
- if (content) onChunk(content);
188
+ if (event.type === 'response.output_text.delta') {
189
+ if (event.delta) onChunk(event.delta);
190
+ } else if (event.type === 'response.completed') {
191
+ responseId = event.response?.id || null;
192
+ }
181
193
  });
182
194
  }).then(() => {
183
195
  finish();
@@ -313,6 +325,10 @@ export class CodexProvider extends Provider {
313
325
  };
314
326
  }
315
327
 
328
+ if (result && item.phase !== undefined) {
329
+ result.phase = item.phase;
330
+ }
331
+
316
332
  // Attach intermediate context estimate so all 7 layers see Codex progress
317
333
  if (result && this._sessionInputTokens > 0) {
318
334
  result.contextUsage = this._sessionInputTokens / this._getMaxContext();
@@ -328,6 +344,7 @@ export class CodexProvider extends Provider {
328
344
  const inputTokens = usage.input_tokens || 0;
329
345
  const outputTokens = usage.output_tokens || 0;
330
346
  const cachedTokens = usage.cached_input_tokens || 0;
347
+ const reasoningTokens = usage.output_tokens_details?.reasoning_tokens || 0;
331
348
  const totalTokens = inputTokens + outputTokens;
332
349
  const cacheCreationTokens = cachedTokens > 0 ? Math.max(0, inputTokens - cachedTokens) : 0;
333
350
 
@@ -352,6 +369,7 @@ export class CodexProvider extends Provider {
352
369
  tokensUsed: totalTokens,
353
370
  inputTokens,
354
371
  outputTokens,
372
+ reasoningTokens,
355
373
  cacheReadTokens: cachedTokens,
356
374
  cacheCreationTokens,
357
375
  contextUsage: inputTokens / maxContext,
@@ -43,6 +43,7 @@ export class Registry extends EventEmitter {
43
43
  lastActivity: null,
44
44
  tokensUsed: 0,
45
45
  contextUsage: 0,
46
+ filesTouched: {},
46
47
  };
47
48
 
48
49
  this.agents.set(agent.id, agent);
@@ -94,6 +95,35 @@ export class Registry extends EventEmitter {
94
95
  return this.getAll().filter((a) => a.teamId === teamId);
95
96
  }
96
97
 
98
+ trackFileOp(id, filePath, op) {
99
+ const agent = this.agents.get(id);
100
+ if (!agent) return;
101
+ if (!agent.filesTouched) agent.filesTouched = {};
102
+ const entry = agent.filesTouched[filePath] || { reads: 0, writes: 0, lastOp: null };
103
+ if (op === 'read') entry.reads++;
104
+ else entry.writes++;
105
+ entry.lastOp = new Date().toISOString();
106
+ agent.filesTouched[filePath] = entry;
107
+
108
+ const keys = Object.keys(agent.filesTouched);
109
+ if (keys.length > 5000) {
110
+ const sorted = keys
111
+ .map((k) => ({ k, t: agent.filesTouched[k].lastOp || '' }))
112
+ .sort((a, b) => a.t.localeCompare(b.t));
113
+ for (let i = 0; i < keys.length - 5000; i++) {
114
+ delete agent.filesTouched[sorted[i].k];
115
+ }
116
+ }
117
+ }
118
+
119
+ getFilesTouched(id) {
120
+ const agent = this.agents.get(id);
121
+ if (!agent || !agent.filesTouched) return [];
122
+ return Object.entries(agent.filesTouched)
123
+ .map(([path, info]) => ({ path, reads: info.reads, writes: info.writes, lastOp: info.lastOp }))
124
+ .sort((a, b) => (b.lastOp || '').localeCompare(a.lastOp || ''));
125
+ }
126
+
97
127
  restore(agents) {
98
128
  for (const agent of agents) {
99
129
  agent.status = 'stopped';
@@ -90,6 +90,9 @@ export function validateAgentConfig(config) {
90
90
 
91
91
  const personality = (typeof config.personality === 'string' && config.personality.length > 0 && config.personality.length <= 64 && NAME_PATTERN.test(config.personality)) ? config.personality : undefined;
92
92
 
93
+ const reasoningEffort = config.reasoning_effort !== undefined && config.reasoning_effort !== null
94
+ ? validateReasoningEffort(config.reasoning_effort) : undefined;
95
+
93
96
  // Return sanitized config (only known fields)
94
97
  return {
95
98
  role: config.role,
@@ -106,6 +109,7 @@ export function validateAgentConfig(config) {
106
109
  integrationApproval,
107
110
  repos,
108
111
  personality,
112
+ reasoningEffort,
109
113
  };
110
114
  }
111
115
 
@@ -195,6 +199,25 @@ export function validateGatewayConfig(config) {
195
199
  };
196
200
  }
197
201
 
202
+ const VALID_REASONING_EFFORTS = ['none', 'low', 'medium', 'high', 'xhigh'];
203
+ const VALID_VERBOSITIES = ['low', 'medium'];
204
+
205
+ export function validateReasoningEffort(value) {
206
+ if (value === null || value === undefined) return null;
207
+ if (typeof value !== 'string' || !VALID_REASONING_EFFORTS.includes(value)) {
208
+ throw new Error(`Invalid reasoning_effort: must be one of ${VALID_REASONING_EFFORTS.join(', ')}`);
209
+ }
210
+ return value;
211
+ }
212
+
213
+ export function validateVerbosity(value) {
214
+ if (value === null || value === undefined) return null;
215
+ if (typeof value !== 'string' || !VALID_VERBOSITIES.includes(value)) {
216
+ throw new Error(`Invalid verbosity: must be one of ${VALID_VERBOSITIES.join(', ')}`);
217
+ }
218
+ return value;
219
+ }
220
+
198
221
  export function escapeMd(text) {
199
222
  if (!text) return '';
200
223
  // Escape markdown special chars that could break table rendering or inject formatting.