xibecode 0.7.6 → 0.9.1

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 (144) hide show
  1. package/README.md +20 -44
  2. package/dist/commands/chat.d.ts.map +1 -1
  3. package/dist/commands/chat.js +9 -1462
  4. package/dist/commands/chat.js.map +1 -1
  5. package/dist/commands/run-pr.d.ts.map +1 -1
  6. package/dist/commands/run-pr.js +9 -1
  7. package/dist/commands/run-pr.js.map +1 -1
  8. package/dist/commands/run.d.ts.map +1 -1
  9. package/dist/commands/run.js +46 -1
  10. package/dist/commands/run.js.map +1 -1
  11. package/dist/components/AssistantMarkdown.d.ts +10 -0
  12. package/dist/components/AssistantMarkdown.d.ts.map +1 -0
  13. package/dist/components/AssistantMarkdown.js +25 -0
  14. package/dist/components/AssistantMarkdown.js.map +1 -0
  15. package/dist/components/design-system/ThemeProvider.d.ts +13 -0
  16. package/dist/components/design-system/ThemeProvider.d.ts.map +1 -0
  17. package/dist/components/design-system/ThemeProvider.js +21 -0
  18. package/dist/components/design-system/ThemeProvider.js.map +1 -0
  19. package/dist/components/design-system/ThemedBox.d.ts +18 -0
  20. package/dist/components/design-system/ThemedBox.d.ts.map +1 -0
  21. package/dist/components/design-system/ThemedBox.js +27 -0
  22. package/dist/components/design-system/ThemedBox.js.map +1 -0
  23. package/dist/components/design-system/ThemedText.d.ts +11 -0
  24. package/dist/components/design-system/ThemedText.d.ts.map +1 -0
  25. package/dist/components/design-system/ThemedText.js +23 -0
  26. package/dist/components/design-system/ThemedText.js.map +1 -0
  27. package/dist/core/agent-tool-policies.d.ts +5 -0
  28. package/dist/core/agent-tool-policies.d.ts.map +1 -0
  29. package/dist/core/agent-tool-policies.js +18 -0
  30. package/dist/core/agent-tool-policies.js.map +1 -0
  31. package/dist/core/agent.d.ts +30 -0
  32. package/dist/core/agent.d.ts.map +1 -1
  33. package/dist/core/agent.js +320 -100
  34. package/dist/core/agent.js.map +1 -1
  35. package/dist/core/background-agent.d.ts.map +1 -1
  36. package/dist/core/background-agent.js +4 -0
  37. package/dist/core/background-agent.js.map +1 -1
  38. package/dist/core/context-compactor.d.ts +10 -0
  39. package/dist/core/context-compactor.d.ts.map +1 -0
  40. package/dist/core/context-compactor.js +158 -0
  41. package/dist/core/context-compactor.js.map +1 -0
  42. package/dist/core/conversation-recovery.d.ts +9 -0
  43. package/dist/core/conversation-recovery.d.ts.map +1 -0
  44. package/dist/core/conversation-recovery.js +15 -0
  45. package/dist/core/conversation-recovery.js.map +1 -0
  46. package/dist/core/debug-workflow.d.ts +9 -0
  47. package/dist/core/debug-workflow.d.ts.map +1 -0
  48. package/dist/core/debug-workflow.js +19 -0
  49. package/dist/core/debug-workflow.js.map +1 -0
  50. package/dist/core/memory-promotions.d.ts +15 -0
  51. package/dist/core/memory-promotions.d.ts.map +1 -0
  52. package/dist/core/memory-promotions.js +38 -0
  53. package/dist/core/memory-promotions.js.map +1 -0
  54. package/dist/core/memory.d.ts +3 -1
  55. package/dist/core/memory.d.ts.map +1 -1
  56. package/dist/core/memory.js +27 -3
  57. package/dist/core/memory.js.map +1 -1
  58. package/dist/core/modes.d.ts +1 -0
  59. package/dist/core/modes.d.ts.map +1 -1
  60. package/dist/core/modes.js +94 -5
  61. package/dist/core/modes.js.map +1 -1
  62. package/dist/core/permission-store.d.ts +15 -0
  63. package/dist/core/permission-store.d.ts.map +1 -0
  64. package/dist/core/permission-store.js +30 -0
  65. package/dist/core/permission-store.js.map +1 -0
  66. package/dist/core/permissions.d.ts +33 -0
  67. package/dist/core/permissions.d.ts.map +1 -0
  68. package/dist/core/permissions.js +139 -0
  69. package/dist/core/permissions.js.map +1 -0
  70. package/dist/core/plan-artifacts.d.ts +10 -0
  71. package/dist/core/plan-artifacts.d.ts.map +1 -0
  72. package/dist/core/plan-artifacts.js +53 -0
  73. package/dist/core/plan-artifacts.js.map +1 -0
  74. package/dist/core/plan-session.d.ts +25 -0
  75. package/dist/core/plan-session.d.ts.map +1 -0
  76. package/dist/core/plan-session.js +95 -0
  77. package/dist/core/plan-session.js.map +1 -0
  78. package/dist/core/planMode.d.ts +5 -0
  79. package/dist/core/planMode.d.ts.map +1 -1
  80. package/dist/core/planMode.js +52 -1
  81. package/dist/core/planMode.js.map +1 -1
  82. package/dist/core/session-bridge.d.ts +12 -1
  83. package/dist/core/session-bridge.d.ts.map +1 -1
  84. package/dist/core/session-bridge.js +46 -0
  85. package/dist/core/session-bridge.js.map +1 -1
  86. package/dist/core/session-manager.d.ts +2 -0
  87. package/dist/core/session-manager.d.ts.map +1 -1
  88. package/dist/core/session-manager.js +3 -1
  89. package/dist/core/session-manager.js.map +1 -1
  90. package/dist/core/swarm.d.ts.map +1 -1
  91. package/dist/core/swarm.js +2 -0
  92. package/dist/core/swarm.js.map +1 -1
  93. package/dist/core/task-status.d.ts +13 -0
  94. package/dist/core/task-status.d.ts.map +1 -0
  95. package/dist/core/task-status.js +17 -0
  96. package/dist/core/task-status.js.map +1 -0
  97. package/dist/core/tool-orchestrator.d.ts +22 -0
  98. package/dist/core/tool-orchestrator.d.ts.map +1 -0
  99. package/dist/core/tool-orchestrator.js +56 -0
  100. package/dist/core/tool-orchestrator.js.map +1 -0
  101. package/dist/core/tools.d.ts +6 -0
  102. package/dist/core/tools.d.ts.map +1 -1
  103. package/dist/core/tools.js +30 -1
  104. package/dist/core/tools.js.map +1 -1
  105. package/dist/core/transcript-cleanup.d.ts +8 -0
  106. package/dist/core/transcript-cleanup.d.ts.map +1 -0
  107. package/dist/core/transcript-cleanup.js +52 -0
  108. package/dist/core/transcript-cleanup.js.map +1 -0
  109. package/dist/ink.d.ts +24 -0
  110. package/dist/ink.d.ts.map +1 -0
  111. package/dist/ink.js +56 -0
  112. package/dist/ink.js.map +1 -0
  113. package/dist/interactiveHelpers.d.ts +9 -0
  114. package/dist/interactiveHelpers.d.ts.map +1 -0
  115. package/dist/interactiveHelpers.js +20 -0
  116. package/dist/interactiveHelpers.js.map +1 -0
  117. package/dist/types/command.d.ts +24 -0
  118. package/dist/types/command.d.ts.map +1 -0
  119. package/dist/types/command.js +2 -0
  120. package/dist/types/command.js.map +1 -0
  121. package/dist/ui/claude-style-chat.d.ts +11 -0
  122. package/dist/ui/claude-style-chat.d.ts.map +1 -0
  123. package/dist/ui/claude-style-chat.js +492 -0
  124. package/dist/ui/claude-style-chat.js.map +1 -0
  125. package/dist/utils/config.d.ts +6 -1
  126. package/dist/utils/config.d.ts.map +1 -1
  127. package/dist/utils/config.js +1 -1
  128. package/dist/utils/config.js.map +1 -1
  129. package/dist/utils/renderOptions.d.ts +7 -0
  130. package/dist/utils/renderOptions.d.ts.map +1 -0
  131. package/dist/utils/renderOptions.js +60 -0
  132. package/dist/utils/renderOptions.js.map +1 -0
  133. package/dist/utils/tool-display.d.ts +8 -0
  134. package/dist/utils/tool-display.d.ts.map +1 -0
  135. package/dist/utils/tool-display.js +213 -0
  136. package/dist/utils/tool-display.js.map +1 -0
  137. package/dist/utils/tui-theme.d.ts +78 -0
  138. package/dist/utils/tui-theme.d.ts.map +1 -0
  139. package/dist/utils/tui-theme.js +76 -0
  140. package/dist/utils/tui-theme.js.map +1 -0
  141. package/dist/webui/server.d.ts.map +1 -1
  142. package/dist/webui/server.js +2 -1
  143. package/dist/webui/server.js.map +1 -1
  144. package/package.json +18 -13
@@ -5,16 +5,20 @@ import { EventEmitter } from 'events';
5
5
  import { MODE_CONFIG, createModeState, transitionMode, ModeOrchestrator, parseModeRequest, stripModeRequests, parseTaskComplete, stripTaskComplete } from './modes.js';
6
6
  import { NeuralMemory } from './memory.js';
7
7
  import { PROVIDER_CONFIGS } from '../utils/config.js';
8
+ import { PermissionManager } from './permissions.js';
9
+ import { ToolOrchestrator } from './tool-orchestrator.js';
10
+ import { compactConversation } from './context-compactor.js';
8
11
  export class LoopDetector {
9
12
  history = [];
10
13
  maxRepeats = 3;
11
14
  timeWindow = 10000;
12
15
  check(toolName, toolInput) {
13
- const signature = JSON.stringify({ tool: toolName, input: toolInput });
16
+ const signature = this.makeSignature(toolName, toolInput);
17
+ const coarse = this.makeCoarseSignature(toolName, toolInput);
14
18
  const now = Date.now();
15
- this.history.push({ tool: toolName, input: signature, timestamp: now });
19
+ this.history.push({ tool: toolName, signature, coarse, timestamp: now });
16
20
  this.history = this.history.filter(h => now - h.timestamp < this.timeWindow);
17
- const recentDuplicates = this.history.filter(h => h.input === signature);
21
+ const recentDuplicates = this.history.filter(h => h.signature === signature);
18
22
  if (recentDuplicates.length >= this.maxRepeats) {
19
23
  return {
20
24
  allowed: false,
@@ -22,7 +26,21 @@ export class LoopDetector {
22
26
  };
23
27
  }
24
28
  const sameTool = this.history.filter(h => h.tool === toolName);
29
+ const sameCoarse = this.history.filter(h => h.coarse === coarse);
30
+ if (sameCoarse.length >= this.maxRepeats + 2) {
31
+ return {
32
+ allowed: false,
33
+ reason: `Loop detected: repeated ${toolName} attempts with near-identical input patterns`,
34
+ };
35
+ }
25
36
  if (sameTool.length >= this.maxRepeats * 2) {
37
+ const uniquePatterns = new Set(sameTool.map(h => h.coarse)).size;
38
+ if (uniquePatterns <= 2) {
39
+ return {
40
+ allowed: false,
41
+ reason: `Loop detected: ${toolName} repeated ${sameTool.length} times with low-variation inputs`,
42
+ };
43
+ }
26
44
  return {
27
45
  allowed: true,
28
46
  reason: `Warning: ${toolName} called ${sameTool.length} times recently`,
@@ -33,6 +51,34 @@ export class LoopDetector {
33
51
  reset() {
34
52
  this.history = [];
35
53
  }
54
+ makeSignature(toolName, input) {
55
+ return JSON.stringify({
56
+ tool: toolName,
57
+ input: this.canonicalize(input),
58
+ });
59
+ }
60
+ makeCoarseSignature(toolName, input) {
61
+ const canonical = this.canonicalize(input);
62
+ if (!canonical || typeof canonical !== 'object' || Array.isArray(canonical)) {
63
+ return `${toolName}:primitive`;
64
+ }
65
+ const keys = Object.keys(canonical).sort();
66
+ return `${toolName}:${keys.join(',')}`;
67
+ }
68
+ canonicalize(value) {
69
+ if (Array.isArray(value)) {
70
+ return value.map((item) => this.canonicalize(item));
71
+ }
72
+ if (value && typeof value === 'object') {
73
+ const inObj = value;
74
+ const out = {};
75
+ for (const key of Object.keys(inObj).sort()) {
76
+ out[key] = this.canonicalize(inObj[key]);
77
+ }
78
+ return out;
79
+ }
80
+ return value;
81
+ }
36
82
  }
37
83
  // ─── Think-tag streaming filter ───────────────────────────────
38
84
  class ThinkTagFilter {
@@ -142,6 +188,9 @@ export class EnhancedAgent extends EventEmitter {
142
188
  contextHintFiles = [];
143
189
  mindsetAdaptive = false;
144
190
  currentMindset = 'convergent';
191
+ permissionManager;
192
+ toolOrchestrator;
193
+ evidenceTrail = [];
145
194
  injectMessage(message) {
146
195
  this.injectedMessages.push(message);
147
196
  }
@@ -151,6 +200,97 @@ export class EnhancedAgent extends EventEmitter {
151
200
  clearInjectedMessages() {
152
201
  this.injectedMessages = [];
153
202
  }
203
+ recordEvidence(kind, detail) {
204
+ this.evidenceTrail.push({ kind, detail, ts: Date.now() });
205
+ if (this.evidenceTrail.length > 80) {
206
+ this.evidenceTrail = this.evidenceTrail.slice(-80);
207
+ }
208
+ }
209
+ estimateMessageTokens(message) {
210
+ const text = (() => {
211
+ if (typeof message.content === 'string')
212
+ return message.content;
213
+ if (!Array.isArray(message.content))
214
+ return '';
215
+ return message.content
216
+ .map((block) => {
217
+ if (block?.type === 'text')
218
+ return String(block.text || '');
219
+ if (block?.type === 'tool_use') {
220
+ const input = block.input ? JSON.stringify(block.input) : '{}';
221
+ return `[tool_use:${String(block.name || 'unknown')}] ${input}`;
222
+ }
223
+ if (block?.type === 'tool_result') {
224
+ return `[tool_result:${String(block.tool_use_id || 'unknown')}] ${String(block.content || '')}`;
225
+ }
226
+ return String(block?.content ?? '');
227
+ })
228
+ .join('\n');
229
+ })();
230
+ // Rough estimate for modern tokenizers across mixed text/code.
231
+ return Math.ceil(text.length / 4);
232
+ }
233
+ estimateConversationTokens() {
234
+ return this.messages.reduce((sum, message) => sum + this.estimateMessageTokens(message), 0);
235
+ }
236
+ hasRecentGroundedEvidence(windowMs = 5 * 60 * 1000) {
237
+ const threshold = Date.now() - windowMs;
238
+ return this.evidenceTrail.some((entry) => entry.ts >= threshold);
239
+ }
240
+ shouldEnforceCompletionEvidence() {
241
+ if (this.config.completionEvidenceMode === 'off')
242
+ return false;
243
+ // Allow single-turn informational answers without tools.
244
+ if (this.toolCallCount === 0 && this.filesChanged.size === 0 && this.iterationCount <= 1) {
245
+ return false;
246
+ }
247
+ return true;
248
+ }
249
+ async postEditVerify(toolExecutor, toolUse, result) {
250
+ if (this.config.postEditVerification === 'off') {
251
+ return { ok: true, message: 'disabled' };
252
+ }
253
+ if (!['write_file', 'edit_file', 'edit_lines', 'verified_edit'].includes(toolUse.name)) {
254
+ return { ok: true, message: 'not-applicable' };
255
+ }
256
+ const input = (toolUse.input ?? {});
257
+ const path = (typeof result?.path === 'string' && result.path) ||
258
+ (typeof input.path === 'string' && input.path) ||
259
+ '';
260
+ if (!path) {
261
+ const strict = this.config.postEditVerification === 'strict';
262
+ return {
263
+ ok: !strict,
264
+ message: strict ? 'post-edit verification failed: missing path' : 'post-edit verification skipped: missing path',
265
+ };
266
+ }
267
+ const readArgs = { path };
268
+ if (typeof input.start_line === 'number' && typeof input.end_line === 'number') {
269
+ readArgs.start_line = input.start_line;
270
+ readArgs.end_line = input.end_line;
271
+ }
272
+ const check = await toolExecutor.execute('read_file', readArgs);
273
+ if (check?.error || check?.success === false) {
274
+ return {
275
+ ok: false,
276
+ message: `post-edit verification failed: could not read ${path}`,
277
+ };
278
+ }
279
+ if (typeof input.new_content === 'string' && input.new_content.trim().length > 0) {
280
+ const observed = typeof check?.content === 'string' ? check.content : '';
281
+ const expected = input.new_content.trim();
282
+ const normalize = (v) => v.replace(/\s+/g, ' ').trim();
283
+ const expectedNeedle = normalize(expected).slice(0, 180);
284
+ if (expectedNeedle && !normalize(observed).includes(expectedNeedle)) {
285
+ return {
286
+ ok: this.config.postEditVerification !== 'strict',
287
+ message: `post-edit verification warning: updated content was not confidently observed in ${path}`,
288
+ };
289
+ }
290
+ }
291
+ this.recordEvidence('post_edit_verify', path);
292
+ return { ok: true, message: `verified ${path}` };
293
+ }
154
294
  // Pricing per 1M tokens (input/output) — Claude models
155
295
  static PRICING = {
156
296
  'claude-sonnet-4-5-20250929': { input: 3, output: 15 },
@@ -174,6 +314,7 @@ export class EnhancedAgent extends EventEmitter {
174
314
  mode: config.mode ?? 'agent',
175
315
  provider: config.provider ?? this.detectProvider(config.model),
176
316
  customProviderFormat: config.customProviderFormat ?? 'openai',
317
+ requestFormat: config.requestFormat ?? 'auto',
177
318
  planFirst: config.planFirst ?? false,
178
319
  sessionMemory: config.sessionMemory,
179
320
  contextHintFiles: config.contextHintFiles ?? [],
@@ -181,13 +322,18 @@ export class EnhancedAgent extends EventEmitter {
181
322
  executionModel: config.executionModel,
182
323
  mindsetAdaptive: config.mindsetAdaptive ?? false,
183
324
  strictTextOnlyCompletion: config.strictTextOnlyCompletion ?? false,
325
+ completionEvidenceMode: config.completionEvidenceMode ?? 'balanced',
326
+ postEditVerification: config.postEditVerification ?? 'balanced',
327
+ memoryRecallMinScore: config.memoryRecallMinScore ?? 2,
184
328
  };
185
329
  this.defaultSkillsPrompt = config.defaultSkillsPrompt ?? '';
186
330
  this.mindsetAdaptive = this.config.mindsetAdaptive ?? false;
187
331
  // Initialize mode state and orchestrator
188
332
  this.modeState = createModeState(this.config.mode);
333
+ this.permissionManager = new PermissionManager(this.config.mode);
334
+ this.toolOrchestrator = new ToolOrchestrator();
189
335
  this.modeOrchestrator = new ModeOrchestrator({
190
- autoApprovalPolicy: 'always', // Allow AI to switch modes autonomously
336
+ autoApprovalPolicy: 'prompt-only',
191
337
  allowAutoEscalation: true,
192
338
  });
193
339
  // Prefer explicit provider override from config, otherwise auto-detect
@@ -243,18 +389,21 @@ export class EnhancedAgent extends EventEmitter {
243
389
  this.iterationCount = 0;
244
390
  this.toolCallCount = 0;
245
391
  this.loopDetector.reset();
392
+ this.evidenceTrail = [];
246
393
  this.messages.push({
247
394
  role: 'user',
248
395
  content: initialPrompt,
249
396
  });
250
397
  // ─── Neural Memory Recall ───
251
398
  try {
252
- const memories = await this.memory.retrieve(initialPrompt);
399
+ const memories = await this.memory.retrieve(initialPrompt, 5, this.config.memoryRecallMinScore);
253
400
  if (memories.length > 0) {
254
401
  const memoryContext = memories.map(m => `- [${new Date(m.timestamp).toISOString().split('T')[0]}] ${m.trigger} -> ${m.action} (${m.outcome})`).join('\n');
255
402
  this.messages.push({
256
403
  role: 'user',
257
- content: `\n\n[Neural Memory Recall]\nI found some relevant past experiences that might help:\n${memoryContext}\n\nUser Prompt: ${initialPrompt}`
404
+ content: `\n\n[Neural Memory Recall UNVERIFIED HINTS]\n` +
405
+ `These are recall hints, not guaranteed facts. Verify with read_file / grep_code / tests before relying on them.\n` +
406
+ `${memoryContext}\n\nUser Prompt: ${initialPrompt}`
258
407
  });
259
408
  this.emit('thinking', { message: `Recalled ${memories.length} relevant memories` });
260
409
  }
@@ -354,15 +503,29 @@ export class EnhancedAgent extends EventEmitter {
354
503
  this.sessionCost += (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
355
504
  }
356
505
  }
357
- // Auto-compact: if conversation exceeds 80 messages, compact older ones
358
- if (this.messages.length > 80) {
359
- const preserved = this.messages.slice(-12);
360
- const compactNotice = {
361
- role: 'assistant',
362
- content: 'Earlier conversation was auto-compacted to save context window space. Recent messages are preserved.',
363
- };
364
- this.messages = [compactNotice, ...preserved];
365
- this.emit('warning', { message: 'Auto-compacted conversation to save context (kept last 12 messages)' });
506
+ // Auto-compact by estimated token budget (OpenClaude-style), not raw message count.
507
+ const compactionThreshold = 120_000;
508
+ const compactionTarget = 90_000;
509
+ let estimatedTokens = this.estimateConversationTokens();
510
+ if (estimatedTokens > compactionThreshold) {
511
+ const beforeCount = this.messages.length;
512
+ const compacted = compactConversation(this.messages, 24);
513
+ this.messages = compacted.messages;
514
+ estimatedTokens = this.estimateConversationTokens();
515
+ this.emit('warning', {
516
+ message: `${compacted.summaryNotice || 'Auto-compacted conversation to save context'} ` +
517
+ `(estimated tokens: ${estimatedTokens})`,
518
+ });
519
+ // If still above target, compact once more with tighter window.
520
+ if (estimatedTokens > compactionTarget && this.messages.length < beforeCount) {
521
+ const compactedAgain = compactConversation(this.messages, 16);
522
+ this.messages = compactedAgain.messages;
523
+ estimatedTokens = this.estimateConversationTokens();
524
+ this.emit('warning', {
525
+ message: `${compactedAgain.summaryNotice || 'Second compaction pass applied'} ` +
526
+ `(estimated tokens: ${estimatedTokens})`,
527
+ });
528
+ }
366
529
  }
367
530
  // Process response
368
531
  const content = response.content;
@@ -384,10 +547,13 @@ export class EnhancedAgent extends EventEmitter {
384
547
  this.modeState = this.modeOrchestrator.requestModeChange(this.modeState, modeRequest.mode, modeRequest.reason, 'model');
385
548
  // Evaluate the request
386
549
  const evaluation = this.modeOrchestrator.evaluateModeChangeRequest(this.modeState);
387
- if (evaluation.approved) {
550
+ const permissionEvaluation = this.permissionManager.evaluateModeTransition(this.modeState.current, modeRequest.mode);
551
+ const approvedByPermission = permissionEvaluation.approved;
552
+ if (evaluation.approved && approvedByPermission) {
388
553
  // Auto-approved - switch immediately
389
554
  const oldMode = this.modeState.current;
390
555
  this.modeState = transitionMode(this.modeState, modeRequest.mode, modeRequest.reason);
556
+ this.permissionManager.setMode(modeRequest.mode);
391
557
  // Update tool executor mode
392
558
  if (toolExecutor.setMode) {
393
559
  toolExecutor.setMode(modeRequest.mode);
@@ -406,7 +572,7 @@ export class EnhancedAgent extends EventEmitter {
406
572
  to: modeRequest.mode,
407
573
  reason: modeRequest.reason,
408
574
  requiresConfirmation: evaluation.requiresConfirmation,
409
- message: evaluation.reason,
575
+ message: permissionEvaluation.reason ?? evaluation.reason,
410
576
  });
411
577
  }
412
578
  }
@@ -415,6 +581,7 @@ export class EnhancedAgent extends EventEmitter {
415
581
  if (taskComplete) {
416
582
  // Switch back to team_leader mode
417
583
  this.modeState = transitionMode(this.modeState, 'team_leader', 'Task completed: ' + taskComplete.summary);
584
+ this.permissionManager.setMode('team_leader');
418
585
  this.emit('mode_changed', {
419
586
  from: this.modeState.previous,
420
587
  to: 'team_leader',
@@ -445,9 +612,27 @@ export class EnhancedAgent extends EventEmitter {
445
612
  }
446
613
  // If no tools, we're done (unless run-pr-style strict completion requires TASK_COMPLETE)
447
614
  if (toolUseBlocks.length === 0) {
615
+ const hasTaskComplete = textBlocks.some((b) => parseTaskComplete(b.text) != null);
616
+ const hasEvidence = this.hasRecentGroundedEvidence();
617
+ if (this.shouldEnforceCompletionEvidence()) {
618
+ const needsTaskComplete = this.config.completionEvidenceMode === 'strict' || this.toolCallCount > 0;
619
+ if ((needsTaskComplete && !hasTaskComplete) || !hasEvidence) {
620
+ const reason = !hasEvidence
621
+ ? 'no recent grounded evidence'
622
+ : 'missing [[TASK_COMPLETE | summary=...]]';
623
+ this.emit('warning', {
624
+ message: `Completion evidence gate blocked finalize: ${reason}.`,
625
+ });
626
+ this.messages.push({
627
+ role: 'user',
628
+ content: '[SYSTEM] Completion gate: before finishing, provide grounded evidence from tool results/tests and then emit ' +
629
+ '[[TASK_COMPLETE | summary=<brief summary> | evidence=<paths/tests/tool proof>]]. Continue working until this is satisfied.',
630
+ });
631
+ continue;
632
+ }
633
+ }
448
634
  if (this.config.strictTextOnlyCompletion &&
449
635
  this.iterationCount < this.config.maxIterations) {
450
- const hasTaskComplete = textBlocks.some((b) => parseTaskComplete(b.text) != null);
451
636
  if (!hasTaskComplete) {
452
637
  this.emit('warning', {
453
638
  message: 'Assistant returned no tool calls without [[TASK_COMPLETE | summary=...]]; nudging to continue.',
@@ -468,69 +653,17 @@ export class EnhancedAgent extends EventEmitter {
468
653
  }
469
654
  // Execute tools
470
655
  const toolResults = [];
471
- for (let i = 0; i < toolUseBlocks.length; i++) {
472
- const toolUse = toolUseBlocks[i];
473
- this.toolCallCount++;
474
- // Loop detection
475
- const loopCheck = this.loopDetector.check(toolUse.name, toolUse.input);
476
- if (!loopCheck.allowed) {
477
- this.emit('warning', { message: loopCheck.reason });
478
- toolResults.push({
479
- type: 'tool_result',
480
- tool_use_id: toolUse.id,
481
- content: `Error: ${loopCheck.reason}. Try a different approach.`,
482
- is_error: true,
483
- });
484
- continue;
485
- }
486
- if (loopCheck.reason) {
487
- this.emit('warning', { message: loopCheck.reason });
488
- }
489
- // Emit tool call
490
- this.emit('tool_call', {
491
- name: toolUse.name,
492
- input: toolUse.input,
493
- index: i + 1,
656
+ const updates = await this.toolOrchestrator.executeBatches(toolUseBlocks, async (toolUse, i) => this.executeSingleToolUse(toolExecutor, toolUse, i));
657
+ for (const update of updates) {
658
+ const payload = typeof update.result === 'string'
659
+ ? update.result
660
+ : JSON.stringify(update.result, null, 2);
661
+ toolResults.push({
662
+ type: 'tool_result',
663
+ tool_use_id: update.toolUse.id,
664
+ content: payload,
665
+ ...(update.success ? {} : { is_error: true }),
494
666
  });
495
- // Execute
496
- try {
497
- const result = await toolExecutor.execute(toolUse.name, toolUse.input);
498
- // Track file changes
499
- if (['write_file', 'edit_file', 'edit_lines', 'verified_edit'].includes(toolUse.name)) {
500
- const input = toolUse.input;
501
- if (typeof input?.path === 'string')
502
- this.filesChanged.add(input.path);
503
- }
504
- const success = !result.error && result.success !== false;
505
- this.emit('tool_result', {
506
- name: toolUse.name,
507
- result,
508
- success,
509
- });
510
- if (this.sessionMemory) {
511
- const msg = typeof result === 'object' && result?.message != null ? String(result.message) : undefined;
512
- this.sessionMemory.recordAttempt(toolUse.name, success, msg);
513
- }
514
- const payload = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
515
- toolResults.push({
516
- type: 'tool_result',
517
- tool_use_id: toolUse.id,
518
- content: payload,
519
- ...(success ? {} : { is_error: true }),
520
- });
521
- }
522
- catch (error) {
523
- this.emit('error', {
524
- tool: toolUse.name,
525
- error: error.message,
526
- });
527
- toolResults.push({
528
- type: 'tool_result',
529
- tool_use_id: toolUse.id,
530
- content: `Error: ${error.message}`,
531
- is_error: true,
532
- });
533
- }
534
667
  }
535
668
  // Add injected messages if any exist
536
669
  if (this.injectedMessages.length > 0) {
@@ -562,19 +695,103 @@ export class EnhancedAgent extends EventEmitter {
562
695
  });
563
696
  }
564
697
  }
698
+ /**
699
+ * Call the model with streaming (fallback to non-streaming).
700
+ */
701
+ async executeSingleToolUse(toolExecutor, toolUse, index) {
702
+ this.toolCallCount++;
703
+ const loopCheck = this.loopDetector.check(toolUse.name, toolUse.input);
704
+ if (!loopCheck.allowed) {
705
+ this.emit('warning', { message: loopCheck.reason });
706
+ return {
707
+ toolUse,
708
+ index,
709
+ result: `Error: ${loopCheck.reason}. Try a different approach.`,
710
+ success: false,
711
+ };
712
+ }
713
+ if (loopCheck.reason) {
714
+ this.emit('warning', { message: loopCheck.reason });
715
+ }
716
+ this.emit('tool_call', {
717
+ name: toolUse.name,
718
+ input: toolUse.input,
719
+ index: index + 1,
720
+ });
721
+ this.recordEvidence('tool_call', toolUse.name);
722
+ try {
723
+ let result = await toolExecutor.execute(toolUse.name, toolUse.input);
724
+ if (['write_file', 'edit_file', 'edit_lines', 'verified_edit'].includes(toolUse.name)) {
725
+ const input = toolUse.input;
726
+ if (typeof input?.path === 'string')
727
+ this.filesChanged.add(input.path);
728
+ }
729
+ const verification = await this.postEditVerify(toolExecutor, toolUse, result);
730
+ if (!verification.ok) {
731
+ const currentMessage = typeof result?.message === 'string' && result.message
732
+ ? `${result.message}; ${verification.message}`
733
+ : verification.message;
734
+ if (result && typeof result === 'object') {
735
+ result = { ...result, message: currentMessage, success: false, error: true, postVerify: verification.message };
736
+ }
737
+ else {
738
+ result = { success: false, error: true, message: currentMessage, result };
739
+ }
740
+ }
741
+ else if (result && typeof result === 'object') {
742
+ result = { ...result, postVerify: verification.message };
743
+ }
744
+ const success = !result?.error && result?.success !== false;
745
+ this.emit('tool_result', {
746
+ name: toolUse.name,
747
+ result,
748
+ success,
749
+ });
750
+ this.recordEvidence(success ? 'tool_result_ok' : 'tool_result_error', `${toolUse.name}:${success ? 'ok' : 'error'}`);
751
+ if (this.sessionMemory) {
752
+ const msg = typeof result === 'object' && result?.message != null ? String(result.message) : undefined;
753
+ this.sessionMemory.recordAttempt(toolUse.name, success, msg);
754
+ }
755
+ return {
756
+ toolUse,
757
+ index,
758
+ result,
759
+ success,
760
+ };
761
+ }
762
+ catch (error) {
763
+ this.emit('error', {
764
+ tool: toolUse.name,
765
+ error: error.message,
766
+ });
767
+ return {
768
+ toolUse,
769
+ index,
770
+ result: `Error: ${error.message}`,
771
+ success: false,
772
+ };
773
+ }
774
+ }
565
775
  /**
566
776
  * Call the model with streaming (fallback to non-streaming).
567
777
  */
568
778
  async callModel(tools) {
569
779
  // Route to provider-specific implementation
570
780
  // Check if the provider uses the Anthropic format or OpenAI format
571
- // Check if the provider uses the Anthropic format or OpenAI format
572
781
  let isAnthropicFormat = false;
573
- if (this.provider !== 'custom') {
574
- isAnthropicFormat = PROVIDER_CONFIGS[this.provider]?.format === 'anthropic';
782
+ const rf = this.config.requestFormat ?? 'auto';
783
+ if (rf === 'openai') {
784
+ isAnthropicFormat = false;
785
+ }
786
+ else if (rf === 'anthropic') {
787
+ isAnthropicFormat = true;
788
+ }
789
+ else if (this.provider !== 'custom') {
790
+ isAnthropicFormat =
791
+ PROVIDER_CONFIGS[this.provider]?.format ===
792
+ 'anthropic';
575
793
  }
576
- // If custom provider, check specific format config
577
- if (this.provider === 'custom') {
794
+ else {
578
795
  isAnthropicFormat = this.config.customProviderFormat === 'anthropic';
579
796
  }
580
797
  // If it's NOT Anthropic format, use the OpenAI-compatible client
@@ -727,27 +944,29 @@ export class EnhancedAgent extends EventEmitter {
727
944
  * Uses streaming (SSE) when available, with a non-streaming fallback.
728
945
  * Supports tools: sends them when provided and normalizes tool_calls in the response to Anthropic-style content blocks.
729
946
  */
730
- async callOpenAI(tools) {
731
- if (!this.config.apiKey) {
732
- // Try to get from specifics if generic is missing, though ConfigManager should have handled this
733
- // strict check might remain here
734
- // throw new Error('API key is required for OpenAI-compatible provider');
735
- }
736
- // Determine Base URL: Config -> Provider Default -> OpenAI Default
947
+ /**
948
+ * OpenAI-compatible chat completions URL. Only appends `/chat/completions` to the
949
+ * configured base do not inject an extra `/v1` segment (base URL must already
950
+ * include any API version prefix the user expects).
951
+ */
952
+ buildOpenAIChatCompletionsUrl() {
737
953
  let base = this.config.baseUrl;
738
954
  if (!base && this.provider && this.provider !== 'custom' && PROVIDER_CONFIGS[this.provider]) {
739
955
  base = PROVIDER_CONFIGS[this.provider].baseUrl;
740
956
  }
741
- base = (base || 'https://api.openai.com').replace(/\/+$/, '');
742
- let url;
743
- // Special handling for some providers that strictly follow /v1 or not
744
- if (base.endsWith('/v1') || base.endsWith('/v4')) {
745
- // Z.ai ends in /v4, others /v1. If already included, just append chat/completions
746
- url = `${base}/chat/completions`;
957
+ base = (base || 'https://api.openai.com/v1').replace(/\/+$/, '');
958
+ if (/\/chat\/completions$/i.test(base)) {
959
+ return base;
747
960
  }
748
- else {
749
- url = `${base}/v1/chat/completions`;
961
+ return `${base}/chat/completions`;
962
+ }
963
+ async callOpenAI(tools) {
964
+ if (!this.config.apiKey) {
965
+ // Try to get from specifics if generic is missing, though ConfigManager should have handled this
966
+ // strict check might remain here
967
+ // throw new Error('API key is required for OpenAI-compatible provider');
750
968
  }
969
+ const url = this.buildOpenAIChatCompletionsUrl();
751
970
  const openAiMessages = this.buildOpenAIMessages();
752
971
  const baseBody = {
753
972
  model: this.getModelForTier(),
@@ -1346,6 +1565,7 @@ When you complete the task, provide a comprehensive summary including:
1346
1565
  - Any trade-offs or design decisions made
1347
1566
  - Potential improvements or follow-up tasks
1348
1567
  - Test results and validation performed
1568
+ - If you used tools, finish with [[TASK_COMPLETE | summary=<brief summary> | evidence=<tool/test proof>]] only when work is actually complete.
1349
1569
 
1350
1570
  ${this.sessionMemory ? this.sessionMemory.getSummary() : ''}
1351
1571
  ${this.mindsetAdaptive ? `\n## Current reasoning mindset: ${this.currentMindset.toUpperCase()}\n${this.currentMindset === 'convergent' ? 'Focus on one solution; narrow options and commit. Use [[SET_MINDSET: divergent]] to explore alternatives, or [[SET_MINDSET: algorithmic]] for step-by-step.' : this.currentMindset === 'divergent' ? 'Explore alternatives; brainstorm. Use [[SET_MINDSET: convergent]] to narrow, or [[SET_MINDSET: algorithmic]] for step-by-step.' : 'Reason step-by-step; formal. Use [[SET_MINDSET: convergent]] to commit, or [[SET_MINDSET: divergent]] to explore.'}\n` : ''}