codeep 2.20.0 → 2.22.0

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 (38) hide show
  1. package/dist/acp/server.js +8 -4
  2. package/dist/api/index.js +26 -19
  3. package/dist/config/index.d.ts +20 -0
  4. package/dist/config/index.js +32 -0
  5. package/dist/renderer/App.js +0 -1
  6. package/dist/renderer/Input.d.ts +0 -1
  7. package/dist/renderer/Input.js +0 -1
  8. package/dist/renderer/commands/registry.js +1 -0
  9. package/dist/renderer/commands.js +19 -3
  10. package/dist/renderer/components/Export.js +0 -2
  11. package/dist/renderer/components/Login.d.ts +0 -1
  12. package/dist/renderer/components/Login.js +0 -2
  13. package/dist/renderer/components/Logout.js +0 -2
  14. package/dist/renderer/components/Settings.d.ts +3 -0
  15. package/dist/renderer/components/Settings.js +0 -12
  16. package/dist/renderer/main.js +35 -56
  17. package/dist/utils/agent.d.ts +7 -0
  18. package/dist/utils/agent.js +102 -6
  19. package/dist/utils/auditLog.d.ts +93 -0
  20. package/dist/utils/auditLog.js +217 -0
  21. package/dist/utils/codeepCloud.d.ts +30 -4
  22. package/dist/utils/codeepCloud.js +71 -19
  23. package/dist/utils/diffPreview.js +0 -1
  24. package/dist/utils/git.js +0 -1
  25. package/dist/utils/headlessReview.d.ts +9 -1
  26. package/dist/utils/headlessReview.js +77 -3
  27. package/dist/utils/mcpStreamableHttp.d.ts +0 -1
  28. package/dist/utils/mcpStreamableHttp.js +0 -3
  29. package/dist/utils/personalities.js +0 -1
  30. package/dist/utils/reviewFix.d.ts +65 -0
  31. package/dist/utils/reviewFix.js +141 -0
  32. package/dist/utils/skillBundles.js +0 -4
  33. package/dist/utils/smartContext.js +0 -18
  34. package/dist/version.d.ts +1 -1
  35. package/dist/version.js +1 -1
  36. package/package.json +2 -2
  37. package/dist/renderer/components/Permission.d.ts +0 -24
  38. package/dist/renderer/components/Permission.js +0 -113
@@ -869,13 +869,17 @@ export function startAcpServer() {
869
869
  transport.error(msg.id, -32602, `Unknown sessionId: ${params.sessionId}`);
870
870
  return;
871
871
  }
872
- const updated = await pullPersonalities();
873
- if (updated === null) {
874
- transport.error(msg.id, -32001, 'Personality sync failed or this device is not linked to codeep.dev.');
872
+ const { describeSyncFailure } = await import('../utils/codeepCloud.js');
873
+ const sync = await pullPersonalities();
874
+ if (!sync.ok) {
875
+ // The old contract collapsed every failure into one message that also
876
+ // covered "not linked", so a client could not tell an expired session
877
+ // from an unreachable server. Say which.
878
+ transport.error(msg.id, -32001, `Personality sync failed — ${describeSyncFailure(sync.reason)}.`);
875
879
  return;
876
880
  }
877
881
  const list = personalityListResult(params.sessionId);
878
- const result = { updated, ...list };
882
+ const result = { updated: sync.count, ...list };
879
883
  transport.respond(msg.id, result);
880
884
  }
881
885
  // ── session/prompt ──────────────────────────────────────────────────────────
package/dist/api/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as http from 'node:http';
2
2
  import * as https from 'node:https';
3
- import { config, getApiKey, resolveBaseUrl } from '../config/index.js';
3
+ import { config, getApiKey, resolveBaseUrl, describeUnsendableKey } from '../config/index.js';
4
4
  import { withRetry, isNetworkError } from '../utils/retry.js';
5
5
  import { checkApiRateLimit } from '../utils/ratelimit.js';
6
6
  import { getProvider, getProviderBaseUrl, getProviderAuthHeader, usesMaxCompletionTokens, requiresDefaultTemperature, modelRejectsSamplingParams, reasoningParamsFor } from '../config/providers.js';
@@ -147,6 +147,28 @@ function parseApiError(status, body) {
147
147
  const truncated = body.length > 200 ? body.slice(0, 200) + '...' : body;
148
148
  return `${status} - ${truncated}`;
149
149
  }
150
+ /**
151
+ * Put the key in the right header, refusing early if it cannot be sent.
152
+ *
153
+ * `fetch` throws "Cannot convert argument to a ByteString because the character
154
+ * at index N…" for a header value outside Latin-1, counting from the start of
155
+ * `Bearer <key>` — so the index points four characters left of where anyone
156
+ * would look, and the message never mentions the key at all. Worse, the caller
157
+ * treats it as a transient API error and retries twice more, which cannot
158
+ * possibly help.
159
+ */
160
+ function applyAuthHeader(headers, apiKey, authHeader) {
161
+ const problem = describeUnsendableKey(apiKey);
162
+ if (problem) {
163
+ throw new ApiError(`This API key cannot be used: ${problem}. Re-copy it and run /login to set it again.`, 400);
164
+ }
165
+ if (authHeader === 'Bearer') {
166
+ headers['Authorization'] = `Bearer ${apiKey}`;
167
+ }
168
+ else {
169
+ headers['x-api-key'] = apiKey;
170
+ }
171
+ }
150
172
  export async function chat(message, history = [], onChunk, onRetry, projectContext, abortSignal) {
151
173
  // Update project context if provided
152
174
  if (projectContext !== undefined) {
@@ -377,12 +399,7 @@ async function chatOpenAI(message, history, model, apiKey, onChunk, abortSignal)
377
399
  const headers = {
378
400
  'Content-Type': 'application/json',
379
401
  };
380
- if (authHeader === 'Bearer') {
381
- headers['Authorization'] = `Bearer ${apiKey}`;
382
- }
383
- else {
384
- headers['x-api-key'] = apiKey;
385
- }
402
+ applyAuthHeader(headers, apiKey, authHeader);
386
403
  // OpenRouter: branding headers + opt in to `usage.cost` so the
387
404
  // chat path reports authoritative per-call cost just like agentChat
388
405
  // does. Kept identical to the agentChat block so the two paths stay
@@ -650,12 +667,7 @@ async function chatAnthropic(message, history, model, apiKey, onChunk, abortSign
650
667
  'Content-Type': 'application/json',
651
668
  'anthropic-version': '2023-06-01',
652
669
  };
653
- if (authHeader === 'Bearer') {
654
- headers['Authorization'] = `Bearer ${apiKey}`;
655
- }
656
- else {
657
- headers['x-api-key'] = apiKey;
658
- }
670
+ applyAuthHeader(headers, apiKey, authHeader);
659
671
  try {
660
672
  // Anthropic prompt caching: wrap system as an array with a
661
673
  // `cache_control` marker so the static system prompt (typically large
@@ -797,12 +809,7 @@ export async function validateApiKey(apiKey, providerId) {
797
809
  const headers = {
798
810
  'Content-Type': 'application/json',
799
811
  };
800
- if (authHeader === 'Bearer') {
801
- headers['Authorization'] = `Bearer ${apiKey}`;
802
- }
803
- else {
804
- headers['x-api-key'] = apiKey;
805
- }
812
+ applyAuthHeader(headers, apiKey, authHeader);
806
813
  if (protocol === 'anthropic') {
807
814
  headers['anthropic-version'] = '2023-06-01';
808
815
  }
@@ -37,6 +37,12 @@ export interface ConfigSchema {
37
37
  * user (reply language, style, stack, preferences). Default true; set false
38
38
  * to keep the profile files but stop injecting them. Managed via `/me`. */
39
39
  userProfile: boolean;
40
+ /** Append a record of what each agent run touched to `.codeep/audit/`.
41
+ * Reads and refusals included — `history.ts` records neither, because it
42
+ * exists to undo writes rather than to say what happened. On unless set
43
+ * false: a record you must remember to enable is not one you can rely on
44
+ * having when you need it. */
45
+ auditLog: boolean;
40
46
  /** Auto-learn: at session save, run one LLM pass to extract durable facts /
41
47
  * preferences about the user and merge them into `~/.codeep/profile.learned.md`
42
48
  * (injected alongside the hand-written profile). OFF by default — opt in via
@@ -318,3 +324,17 @@ export declare function loadProfile(name: string): Profile | null;
318
324
  export declare function applyProfile(profile: Profile): void;
319
325
  export declare function listProfiles(): string[];
320
326
  export declare function deleteProfile(name: string): boolean;
327
+ /**
328
+ * Whether a key can survive being put in an HTTP header.
329
+ *
330
+ * `fetch` encodes header values as Latin-1 and throws "Cannot convert argument
331
+ * to a ByteString" on anything outside it. A key pasted from a web page or a
332
+ * chat message can pick up a non-breaking space, a zero-width character or a
333
+ * curly quote, and the resulting failure names neither the key nor the
334
+ * character — only "the character at index N", counted across the whole header
335
+ * value with `Bearer ` included, which is not where anyone would look.
336
+ *
337
+ * Returns null when the key is fine, or a description that locates the problem
338
+ * without ever reproducing the key itself.
339
+ */
340
+ export declare function describeUnsendableKey(apiKey: string): string | null;
@@ -175,6 +175,7 @@ function createConfig() {
175
175
  autoSessionTitle: true,
176
176
  autoSummarizeHistory: true,
177
177
  userProfile: true,
178
+ auditLog: true,
178
179
  autoLearnProfile: false,
179
180
  trustedHookProjects: [],
180
181
  currentSessionId: '',
@@ -1264,3 +1265,34 @@ export function deleteProfile(name) {
1264
1265
  return false;
1265
1266
  }
1266
1267
  }
1268
+ /**
1269
+ * Whether a key can survive being put in an HTTP header.
1270
+ *
1271
+ * `fetch` encodes header values as Latin-1 and throws "Cannot convert argument
1272
+ * to a ByteString" on anything outside it. A key pasted from a web page or a
1273
+ * chat message can pick up a non-breaking space, a zero-width character or a
1274
+ * curly quote, and the resulting failure names neither the key nor the
1275
+ * character — only "the character at index N", counted across the whole header
1276
+ * value with `Bearer ` included, which is not where anyone would look.
1277
+ *
1278
+ * Returns null when the key is fine, or a description that locates the problem
1279
+ * without ever reproducing the key itself.
1280
+ */
1281
+ export function describeUnsendableKey(apiKey) {
1282
+ for (let i = 0; i < apiKey.length; i++) {
1283
+ const code = apiKey.charCodeAt(i);
1284
+ if (code > 0xFF) {
1285
+ const name = code === 0x200B ? 'a zero-width space'
1286
+ : code === 0x2018 || code === 0x2019 ? 'a curly quote'
1287
+ : code === 0x201C || code === 0x201D ? 'a curly double quote'
1288
+ : `U+${code.toString(16).toUpperCase().padStart(4, '0')}`;
1289
+ return `character ${i + 1} of the key is ${name}, which cannot be sent in an HTTP header`;
1290
+ }
1291
+ // 0xA0 is inside Latin-1 and technically sendable, but a non-breaking space
1292
+ // in a key is never intentional and produces a 401 that reads as a bad key.
1293
+ if (code === 0xA0) {
1294
+ return `character ${i + 1} of the key is a non-breaking space — probably picked up when copying`;
1295
+ }
1296
+ }
1297
+ return null;
1298
+ }
@@ -1393,7 +1393,6 @@ export class App {
1393
1393
  mentionItemCount: this.mention.items.length,
1394
1394
  });
1395
1395
  const layout = chatLayout(height, panelHeight);
1396
- const mainHeight = layout.mainHeight;
1397
1396
  const headerHeight = width >= 60 && height >= 16 ? 2 : 0;
1398
1397
  const messagesStart = Math.min(layout.messagesEnd, headerHeight);
1399
1398
  const messagesEnd = layout.messagesEnd;
@@ -13,7 +13,6 @@ export interface KeyEvent {
13
13
  export type KeyHandler = (event: KeyEvent) => void;
14
14
  export declare class Input {
15
15
  private handlers;
16
- private rl;
17
16
  private dataHandler;
18
17
  /**
19
18
  * Start listening for input
@@ -4,7 +4,6 @@
4
4
  */
5
5
  export class Input {
6
6
  handlers = [];
7
- rl = null;
8
7
  dataHandler = null;
9
8
  /**
10
9
  * Start listening for input
@@ -205,6 +205,7 @@ export const COMMANDS = [
205
205
  usage: ['init [project]', 'learn [on|off]', 'sync', 'off', 'forget'],
206
206
  },
207
207
  { name: 'agents', description: 'List sub-agents the agent can delegate to (researcher / reviewer / tester / your own)', category: 'settings' },
208
+ { name: 'audit', description: 'What agents did in this project — runs, tools used, and anything the boundary refused', category: 'settings', usage: ['on', 'off'] },
208
209
  { name: 'insights', description: 'Activity summary — runs, files, tools, projects over the last N days (default 7)', category: 'settings', usage: ['--days N'] },
209
210
  { name: 'openrouter', description: 'OpenRouter routing prefs (prefer/ignore providers, fallbacks, privacy)', category: 'settings' },
210
211
  // ── extensions & mcp ───────────────────────────────────────────────────────
@@ -445,6 +445,23 @@ export async function handleCommand(command, args, ctx) {
445
445
  });
446
446
  break;
447
447
  }
448
+ case 'audit': {
449
+ const { formatAuditLog } = await import('../utils/auditLog.js');
450
+ const sub = args[0]?.toLowerCase();
451
+ if (sub === 'on' || sub === 'off') {
452
+ config.set('auditLog', sub === 'on');
453
+ ctx.app.notify(sub === 'on'
454
+ ? 'Audit recording on — runs are recorded to .codeep/audit/.'
455
+ : 'Audit recording off. Records already written are kept.');
456
+ break;
457
+ }
458
+ if (sub) {
459
+ ctx.app.notify(`Unknown audit subcommand: ${sub}. Use /audit, /audit on, or /audit off`);
460
+ break;
461
+ }
462
+ ctx.app.addMessage({ role: 'system', content: formatAuditLog(ctx.projectPath) });
463
+ break;
464
+ }
448
465
  case 'agents': {
449
466
  // List sub-agents the agent can `delegate` to (built-in + .codeep/agents/).
450
467
  const { formatAgentList } = await import('../utils/agents.js');
@@ -2102,7 +2119,6 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2102
2119
  }
2103
2120
  case 'profile': {
2104
2121
  const subCmd = args[0]?.toLowerCase();
2105
- const profileName = args[1] || args[0]; // /profile save name OR /profile name
2106
2122
  if (!subCmd || subCmd === 'list') {
2107
2123
  const profiles = listProfiles();
2108
2124
  if (profiles.length === 0) {
@@ -2165,9 +2181,9 @@ Describe what this skill does. The agent reads this body verbatim when it invoke
2165
2181
  }
2166
2182
  case 'sync': {
2167
2183
  const subCmd = args[0]?.toLowerCase() || 'all';
2168
- const { pushLearning, pullLearning, pushProfiles, pullProfiles } = await import('../utils/codeepCloud.js');
2184
+ const { pushLearning, pushProfiles, pullProfiles } = await import('../utils/codeepCloud.js');
2169
2185
  const { getSyncToken } = await import('../config/index.js');
2170
- const { loadGlobalPreferences, saveGlobalPreferences } = await import('../utils/learning.js');
2186
+ const { loadGlobalPreferences } = await import('../utils/learning.js');
2171
2187
  if (!getSyncToken()) {
2172
2188
  ctx.app.notify('Not linked to codeep.dev. Run: codeep account');
2173
2189
  break;
@@ -2,8 +2,6 @@
2
2
  * Export panel component
3
3
  */
4
4
  import { fg, style } from '../ansi.js';
5
- // Primary color: #f02a30 (Codeep red)
6
- const PRIMARY_COLOR = fg.rgb(240, 42, 48);
7
5
  const FORMATS = [
8
6
  { id: 'md', name: 'Markdown', desc: 'Formatted with headers and separators' },
9
7
  { id: 'json', name: 'JSON', desc: 'Structured data format' },
@@ -15,7 +15,6 @@ export interface LoginOptions {
15
15
  */
16
16
  export declare class LoginScreen {
17
17
  private screen;
18
- private input;
19
18
  private editor;
20
19
  private options;
21
20
  private showKey;
@@ -14,13 +14,11 @@ const PRIMARY_BRIGHT = fg.rgb(255, 80, 85);
14
14
  */
15
15
  export class LoginScreen {
16
16
  screen;
17
- input;
18
17
  editor;
19
18
  options;
20
19
  showKey = false;
21
20
  constructor(screen, input, options) {
22
21
  this.screen = screen;
23
- this.input = input;
24
22
  this.editor = new LineEditor();
25
23
  this.options = options;
26
24
  }
@@ -2,8 +2,6 @@
2
2
  * Logout panel component
3
3
  */
4
4
  import { fg, style } from '../ansi.js';
5
- // Primary color: #f02a30 (Codeep red)
6
- const PRIMARY_COLOR = fg.rgb(240, 42, 48);
7
5
  /**
8
6
  * Render inline logout picker
9
7
  */
@@ -24,6 +24,9 @@ export interface SettingsState {
24
24
  editing: boolean;
25
25
  editValue: string;
26
26
  }
27
+ /**
28
+ * Format value for display
29
+ */
27
30
  /**
28
31
  * Handle settings key
29
32
  * Returns: { handled: boolean, close: boolean, notify?: string }
@@ -1,12 +1,8 @@
1
1
  /**
2
2
  * Settings screen component
3
3
  */
4
- import { fg } from '../ansi.js';
5
4
  import { config } from '../../config/index.js';
6
5
  import { updateRateLimits } from '../../utils/ratelimit.js';
7
- // Primary color: #f02a30 (Codeep red)
8
- const PRIMARY_COLOR = fg.rgb(240, 42, 48);
9
- const PRIMARY_BRIGHT = fg.rgb(255, 80, 85);
10
6
  /**
11
7
  * Write a value to the config for a given setting.
12
8
  *
@@ -295,14 +291,6 @@ export const SETTINGS = [
295
291
  /**
296
292
  * Format value for display
297
293
  */
298
- function formatValue(setting) {
299
- const value = setting.getValue();
300
- if (setting.type === 'select' && setting.options) {
301
- const option = setting.options.find(o => o.value === value);
302
- return option ? option.label : String(value);
303
- }
304
- return String(value);
305
- }
306
294
  /**
307
295
  * Handle settings key
308
296
  * Returns: { handled: boolean, close: boolean, notify?: string }
@@ -9,7 +9,6 @@ import { App } from './App.js';
9
9
  import { Screen } from './Screen.js';
10
10
  import { Input } from './Input.js';
11
11
  import { LoginScreen, renderProviderSelect } from './components/Login.js';
12
- import { renderPermissionScreen, getPermissionOptions } from './components/Permission.js';
13
12
  import { chat, setProjectContext } from '../api/index.js';
14
13
  import { getZaiVisionConfig, getMinimaxMcpConfig, callZaiVisionApi, callMinimaxApi } from '../utils/mcpIntegration.js';
15
14
  import { config, loadApiKey, loadAllApiKeys, getCurrentProvider, autoSaveSession, startNewSession, getCurrentSessionId, loadSession, listSessionsWithInfo, deleteSession, hasReadPermission, hasWritePermission, setProjectPermission, initializeAsProject, isManuallyInitializedProject, setApiKey, setProvider, getGithubId, } from '../config/index.js';
@@ -384,49 +383,6 @@ async function showLoginFlow() {
384
383
  renderCurrentStep();
385
384
  });
386
385
  }
387
- // ─── Permission flow (full-screen, pre-app) ───────────────────────────────────
388
- async function showPermissionFlow() {
389
- return new Promise((resolve) => {
390
- const screen = new Screen();
391
- const input = new Input();
392
- let selectedIndex = 0;
393
- const options = getPermissionOptions();
394
- const isProject = isProjectDirectory(projectPath);
395
- const currentPermission = hasWritePermission(projectPath)
396
- ? 'write'
397
- : hasReadPermission(projectPath)
398
- ? 'read'
399
- : 'none';
400
- screen.init();
401
- input.start();
402
- const cleanup = () => { input.stop(); screen.cleanup(); };
403
- const render = () => {
404
- renderPermissionScreen(screen, {
405
- projectPath, isProject, currentPermission,
406
- onSelect: () => { }, onCancel: () => { },
407
- }, selectedIndex);
408
- };
409
- input.onKey((event) => {
410
- if (event.key === 'up') {
411
- selectedIndex = Math.max(0, selectedIndex - 1);
412
- render();
413
- }
414
- else if (event.key === 'down') {
415
- selectedIndex = Math.min(options.length - 1, selectedIndex + 1);
416
- render();
417
- }
418
- else if (event.key === 'enter') {
419
- cleanup();
420
- resolve(options[selectedIndex]);
421
- }
422
- else if (event.key === 'escape') {
423
- cleanup();
424
- resolve('none');
425
- }
426
- });
427
- render();
428
- });
429
- }
430
386
  // ─── Session picker ───────────────────────────────────────────────────────────
431
387
  function showSessionPickerInline() {
432
388
  const sessions = listSessionsWithInfo(projectPath);
@@ -544,16 +500,32 @@ Commands (in chat):
544
500
  // the user profile. Web-edited personalities replace their local copy
545
501
  // after a safety backup; commands/profile retain additive merge rules.
546
502
  const { pullPersonalities, pullCommands, pullUserProfile, getLastPersonalityPullBackupCount } = await import('../utils/codeepCloud.js');
547
- const pCount = await pullPersonalities();
548
- if (typeof pCount === 'number' && pCount > 0) {
549
- console.log(` Pulled ${pCount} personalit${pCount === 1 ? 'y' : 'ies'}.`);
503
+ // Report all three outcomes, not just the interesting one. Printing only
504
+ // on count > 0 made a failed sync look identical to a sync with nothing
505
+ // new — silence meant either, and the user could not tell which.
506
+ const { describeSyncFailure } = await import('../utils/codeepCloud.js');
507
+ const personalities = await pullPersonalities();
508
+ if (!personalities.ok) {
509
+ console.log(` Could not pull agents — ${describeSyncFailure(personalities.reason)}.`);
510
+ }
511
+ else if (personalities.count > 0) {
512
+ console.log(` Pulled ${personalities.count} personalit${personalities.count === 1 ? 'y' : 'ies'}.`);
550
513
  const backups = getLastPersonalityPullBackupCount();
551
514
  if (backups > 0)
552
515
  console.log(` Backed up ${backups} replaced local cop${backups === 1 ? 'y' : 'ies'} in ~/.codeep/backups/personalities/.`);
553
516
  }
554
- const cCount = await pullCommands();
555
- if (typeof cCount === 'number' && cCount > 0) {
556
- console.log(` Pulled ${cCount} custom command${cCount === 1 ? '' : 's'}.`);
517
+ else if (personalities.removed === 0) {
518
+ console.log(' Agents already up to date.');
519
+ }
520
+ if (personalities.ok && personalities.removed > 0) {
521
+ console.log(` Removed ${personalities.removed} agent${personalities.removed === 1 ? '' : 's'} deleted on codeep.dev (backed up first).`);
522
+ }
523
+ const commands = await pullCommands();
524
+ if (!commands.ok) {
525
+ console.log(` Could not pull custom commands — ${describeSyncFailure(commands.reason)}.`);
526
+ }
527
+ else if (commands.count > 0) {
528
+ console.log(` Pulled ${commands.count} custom command${commands.count === 1 ? '' : 's'}.`);
557
529
  }
558
530
  const profPulled = await pullUserProfile();
559
531
  if (profPulled === 1) {
@@ -597,13 +569,20 @@ Commands (in chat):
597
569
  }
598
570
  // Also push portable personal config — personalities + commands + profile.
599
571
  const { pushPersonalities, pushCommands, pushUserProfile } = await import('../utils/codeepCloud.js');
600
- const pCount = await pushPersonalities();
601
- if (typeof pCount === 'number' && pCount > 0) {
602
- console.log(` Pushed ${pCount} personalit${pCount === 1 ? 'y' : 'ies'}.`);
572
+ const { describeSyncFailure } = await import('../utils/codeepCloud.js');
573
+ const personalities = await pushPersonalities();
574
+ if (!personalities.ok) {
575
+ console.log(` Could not push agents — ${describeSyncFailure(personalities.reason)}.`);
576
+ }
577
+ else if (personalities.count > 0) {
578
+ console.log(` Pushed ${personalities.count} personalit${personalities.count === 1 ? 'y' : 'ies'}.`);
579
+ }
580
+ const commands = await pushCommands();
581
+ if (!commands.ok) {
582
+ console.log(` Could not push custom commands — ${describeSyncFailure(commands.reason)}.`);
603
583
  }
604
- const cCount = await pushCommands();
605
- if (typeof cCount === 'number' && cCount > 0) {
606
- console.log(` Pushed ${cCount} custom command${cCount === 1 ? '' : 's'}.`);
584
+ else if (commands.count > 0) {
585
+ console.log(` Pushed ${commands.count} custom command${commands.count === 1 ? '' : 's'}.`);
607
586
  }
608
587
  if (await pushUserProfile()) {
609
588
  console.log(' Pushed your profile (about you).');
@@ -6,6 +6,7 @@
6
6
  import { ProjectContext } from './project';
7
7
  import { loadProjectRules, loadProgressLog, writeProgressLog, formatChatHistoryForAgent } from './agentChat';
8
8
  import type { AgentChatResponse } from './agentChat';
9
+ import { type Personality } from './personalities';
9
10
  export { loadProjectRules, loadProgressLog, writeProgressLog, formatChatHistoryForAgent };
10
11
  export type { AgentChatResponse };
11
12
  import { ToolCall, ToolResult, ActionLog } from './tools';
@@ -83,6 +84,12 @@ export interface AgentOptions {
83
84
  * sub-agent's tool actions still record into the parent's session, so undo
84
85
  * spans delegation. */
85
86
  nested?: boolean;
87
+ /** Run under this capability boundary instead of whatever the user has
88
+ * selected. Used by non-interactive callers that must pin the boundary
89
+ * themselves — a CI fix, for example, runs files+tests regardless of the
90
+ * machine's active bot. Enforced by the same gate as any other bot; this
91
+ * chooses which one applies, never whether one does. */
92
+ personalityOverride?: Personality;
86
93
  /** Delegation depth. 0 = top-level orchestrator (gets the `delegate` tool);
87
94
  * sub-agents run at depth 1 and cannot delegate further (v1). */
88
95
  depth?: number;