codeep 2.1.4 → 2.3.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.
@@ -13,6 +13,7 @@ const debug = (...args) => {
13
13
  // Import chat layer (prompt building + API calls)
14
14
  import { agentChat, getAgentSystemPrompt, getFallbackSystemPrompt, loadProjectRules, loadProgressLog, writeProgressLog, formatChatHistoryForAgent, summarizeEarlierHistory, } from './agentChat.js';
15
15
  import { ApiError } from '../api/index.js';
16
+ import { loadUserProfilePrompt } from './userProfile.js';
16
17
  export { loadProjectRules, loadProgressLog, writeProgressLog, formatChatHistoryForAgent };
17
18
  /**
18
19
  * Calculate dynamic timeout based on task complexity
@@ -127,8 +128,10 @@ export async function runAgent(prompt, projectContext, options = {}) {
127
128
  const startTime = Date.now();
128
129
  const actions = [];
129
130
  const messages = [];
130
- // Start history session for undo support
131
- const sessionId = startSession(prompt, projectContext.root || process.cwd());
131
+ // Start history session for undo support. Skipped for nested (delegated)
132
+ // runs so we don't reset the parent's currentSession singleton — the
133
+ // sub-agent's actions still record into the parent's open session.
134
+ const sessionId = opts.nested ? '' : startSession(prompt, projectContext.root || process.cwd());
132
135
  // Task planning phase (if enabled)
133
136
  // Use planning for complex keywords or multi-word prompts
134
137
  let taskPlan = null;
@@ -221,10 +224,53 @@ export async function runAgent(prompt, projectContext, options = {}) {
221
224
  catch {
222
225
  // Skill loading failure shouldn't fail the whole agent run.
223
226
  }
227
+ // Sub-agents — the `delegate` tool lets the top-level agent hand a
228
+ // self-contained sub-task to a specialist that runs in its own context and
229
+ // returns a summary. Only advertised at depth 0, so sub-agents can't recurse
230
+ // (delegation depth is capped at 1 for v1).
231
+ let agentsCatalogBlock = '';
232
+ if ((opts.depth ?? 0) === 0) {
233
+ try {
234
+ const { loadAgents, formatAgentsForSysprompt } = await import('./agents.js');
235
+ const agents = loadAgents(projectContext.root);
236
+ if (agents.length > 0) {
237
+ mcpToolDefs.push({
238
+ name: 'delegate',
239
+ description: 'Delegate a self-contained sub-task to a specialist sub-agent that runs in its own fresh context and returns a summary. Use it to keep your own context focused.',
240
+ inputSchema: {
241
+ type: 'object',
242
+ properties: {
243
+ agent: { type: 'string', description: 'Sub-agent name from the catalog (e.g. "researcher"). Omit for a general-purpose sub-agent.' },
244
+ task: { type: 'string', description: 'A clear, self-contained instruction for the sub-agent.' },
245
+ },
246
+ required: ['task'],
247
+ },
248
+ });
249
+ agentsCatalogBlock = formatAgentsForSysprompt(agents);
250
+ }
251
+ }
252
+ catch {
253
+ // Agent loading must never block the run.
254
+ }
255
+ }
224
256
  // Build system prompt - use fallback format if native tools not supported
225
257
  let systemPrompt = useNativeTools
226
258
  ? getAgentSystemPrompt(projectContext)
227
259
  : getFallbackSystemPrompt(projectContext, mcpToolDefs);
260
+ // Delegated sub-agent role — its defining instruction. Injected right after
261
+ // the base prompt so it frames everything that follows. Empty for normal runs.
262
+ if (opts.roleAddendum) {
263
+ systemPrompt += '\n\n## Your role (delegated sub-agent)\n' + opts.roleAddendum;
264
+ }
265
+ // Inject the user profile (global ~/.codeep/profile.md + project
266
+ // .codeep/profile.md) so the agent adapts to who it's working with —
267
+ // reply language, style, stack, hard preferences. User-authored and gated
268
+ // by config.userProfile. Lives here (not in the base prompt) so every
269
+ // surface — CLI, ACP, VS Code, Zed — inherits it via this single path.
270
+ const userProfileBlock = loadUserProfilePrompt(projectContext.root);
271
+ if (userProfileBlock) {
272
+ systemPrompt += userProfileBlock;
273
+ }
228
274
  // Inject project rules (from .codeep/rules.md or CODEEP.md)
229
275
  const projectRules = loadProjectRules(projectContext.root);
230
276
  if (projectRules) {
@@ -259,6 +305,10 @@ export async function runAgent(prompt, projectContext, options = {}) {
259
305
  if (skillCatalogBlock) {
260
306
  systemPrompt += '\n\n' + skillCatalogBlock;
261
307
  }
308
+ // Sub-agent catalog (delegate) — only present at depth 0.
309
+ if (agentsCatalogBlock) {
310
+ systemPrompt += agentsCatalogBlock;
311
+ }
262
312
  // Active personality goes LAST — appended after skills / project rules /
263
313
  // smart context so its tone overrides earlier conventions. Set via
264
314
  // `/personality <name>`; empty when no personality is active.
@@ -297,6 +347,95 @@ export async function runAgent(prompt, projectContext, options = {}) {
297
347
  ...(config.get('agentConfirmExecuteCommand') !== false ? ['execute_command'] : []),
298
348
  ...(config.get('agentConfirmWriteFile') === true ? ['write_file', 'edit_file'] : []),
299
349
  ]);
350
+ // Delegation handler: run a named (or generic) sub-agent in its own fresh
351
+ // context and return its summary as the tool result. Reachable only when the
352
+ // `delegate` tool was advertised (depth 0). The sub-agent runs nested (no own
353
+ // undo session) at depth 1 and never gets `delegate`, so depth is capped at 1.
354
+ const runDelegate = async (toolCall) => {
355
+ const params = (toolCall.parameters || {});
356
+ const task = String(params.task || '').trim();
357
+ const fail = (error) => ({ success: false, output: '', error, tool: 'delegate', parameters: toolCall.parameters });
358
+ if (!task)
359
+ return fail('delegate requires a non-empty "task".');
360
+ let def = null;
361
+ try {
362
+ const { findAgent } = await import('./agents.js');
363
+ def = params.agent ? findAgent(params.agent, projectContext.root) : null;
364
+ if (params.agent && !def)
365
+ return fail(`No sub-agent named "${params.agent}". Run /agents to see available agents.`);
366
+ }
367
+ catch { /* fall back to a generic sub-agent */ }
368
+ let roleAddendum = def?.prompt
369
+ || 'You are a general-purpose sub-agent. Complete the task in your own context and return a concise, self-contained summary of what you did and the outcome.';
370
+ if (def?.tools)
371
+ roleAddendum += `\n\nYou may use ONLY these tools: ${def.tools.join(', ')}.`;
372
+ if (def?.personality) {
373
+ try {
374
+ const { findPersonality } = await import('./personalities.js');
375
+ const p = findPersonality(def.personality, projectContext.root);
376
+ if (p)
377
+ roleAddendum += '\n' + p.prompt;
378
+ }
379
+ catch { /* ignore */ }
380
+ }
381
+ const label = def?.name || 'agent';
382
+ opts.onIteration?.(iteration, `⤷ delegating to ${label}…`);
383
+ const tag = (text) => `⤷ ${label}: ${text}`;
384
+ // Model override — swap config for the nested run, restore in finally.
385
+ const prevModel = config.get('model');
386
+ const prevProvider = config.get('provider');
387
+ let swapped = false;
388
+ if (def?.model) {
389
+ try {
390
+ const m = String(def.model);
391
+ if (m.includes('/')) {
392
+ const { setProvider } = await import('../config/index.js');
393
+ setProvider(m.slice(0, m.indexOf('/')));
394
+ config.set('model', m.slice(m.indexOf('/') + 1));
395
+ }
396
+ else {
397
+ config.set('model', m);
398
+ }
399
+ swapped = true;
400
+ }
401
+ catch { /* keep parent's model */ }
402
+ }
403
+ try {
404
+ const sub = await runAgent(task, projectContext, {
405
+ ...DEFAULT_OPTIONS,
406
+ nested: true,
407
+ depth: (opts.depth ?? 0) + 1,
408
+ allowedTools: def?.tools,
409
+ roleAddendum,
410
+ maxIterations: def?.maxIterations ?? Math.min(15, opts.maxIterations),
411
+ maxDuration: opts.maxDuration,
412
+ abortSignal: opts.abortSignal,
413
+ onRequestPermission: opts.onRequestPermission,
414
+ onExecuteCommand: opts.onExecuteCommand,
415
+ fs: opts.fs,
416
+ mcpSessionId: opts.mcpSessionId,
417
+ autoVerify: false,
418
+ onIteration: (_i, msg) => opts.onIteration?.(iteration, tag(msg)),
419
+ onThinking: (t) => opts.onThinking?.(tag(t)),
420
+ // No chatHistory → the sub-agent gets a fresh context window.
421
+ });
422
+ const summary = sub.finalResponse?.trim() || '(sub-agent finished without a summary)';
423
+ return { success: sub.success, output: `[${label}] ${summary}`, tool: 'delegate', parameters: toolCall.parameters };
424
+ }
425
+ catch (err) {
426
+ return fail(`Sub-agent "${label}" failed: ${err.message}`);
427
+ }
428
+ finally {
429
+ if (swapped) {
430
+ try {
431
+ const { setProvider } = await import('../config/index.js');
432
+ setProvider(String(prevProvider));
433
+ config.set('model', prevModel);
434
+ }
435
+ catch { /* ignore restore failure */ }
436
+ }
437
+ }
438
+ };
300
439
  const maxTimeoutRetries = 3;
301
440
  const maxConsecutiveTimeouts = 30; // Allow more consecutive timeouts before giving up
302
441
  const maxConsecutiveRateLimits = 5; // Stop after 5 consecutive rate-limited iterations
@@ -328,7 +467,8 @@ export async function runAgent(prompt, projectContext, options = {}) {
328
467
  finalResponse: partialLines.join('\n'),
329
468
  error: `Exceeded maximum duration of ${durationMin} min`,
330
469
  };
331
- writeProgressLog(projectContext.root || '', prompt, result, projectContext.name);
470
+ if (!opts.nested)
471
+ writeProgressLog(projectContext.root || '', prompt, result, projectContext.name);
332
472
  return result;
333
473
  }
334
474
  // Check abort signal
@@ -590,6 +730,21 @@ export async function runAgent(prompt, projectContext, options = {}) {
590
730
  const toolResults = [];
591
731
  for (const toolCall of toolCalls) {
592
732
  opts.onToolCall?.(toolCall);
733
+ // Tool scoping for delegated sub-agents: reject any tool outside the
734
+ // agent's allowlist up front — no permission prompt, no execution.
735
+ if (opts.allowedTools && !opts.allowedTools.includes(toolCall.tool)) {
736
+ const denied = {
737
+ success: false,
738
+ output: '',
739
+ error: `Tool "${toolCall.tool}" is not available to this sub-agent.`,
740
+ tool: toolCall.tool,
741
+ parameters: toolCall.parameters,
742
+ };
743
+ opts.onToolResult?.(denied, toolCall);
744
+ actions.push(createActionLog(toolCall, denied));
745
+ toolResults.push(`Tool ${toolCall.tool} is not allowed for this sub-agent. Use only: ${opts.allowedTools.join(', ')}.`);
746
+ continue;
747
+ }
593
748
  // Permission check for dangerous tools (only when callback is provided, e.g. ACP/Zed)
594
749
  if (opts.onRequestPermission && dangerousTools.has(toolCall.tool) && !alwaysAllowedTools.has(toolCall.tool)) {
595
750
  const rejectResult = () => {
@@ -625,7 +780,10 @@ export async function runAgent(prompt, projectContext, options = {}) {
625
780
  }
626
781
  }
627
782
  let toolResult;
628
- if (opts.dryRun) {
783
+ if (toolCall.tool === 'delegate') {
784
+ toolResult = await runDelegate(toolCall);
785
+ }
786
+ else if (opts.dryRun) {
629
787
  toolResult = {
630
788
  success: true,
631
789
  output: `[DRY RUN] Would execute: ${toolCall.tool}`,
@@ -750,7 +908,8 @@ export async function runAgent(prompt, projectContext, options = {}) {
750
908
  finalResponse: partialLines.join('\n'),
751
909
  error: `Exceeded maximum of ${opts.maxIterations} iterations`,
752
910
  };
753
- writeProgressLog(projectContext.root || '', prompt, result, projectContext.name);
911
+ if (!opts.nested)
912
+ writeProgressLog(projectContext.root || '', prompt, result, projectContext.name);
754
913
  return result;
755
914
  }
756
915
  // Self-verification: Run build/test and fix errors if needed
@@ -876,13 +1035,39 @@ export async function runAgent(prompt, projectContext, options = {}) {
876
1035
  }
877
1036
  }
878
1037
  }
1038
+ // Pipeline (Phase 2): optional automatic review pass. After a top-level run
1039
+ // that changed files, delegate to the `reviewer` sub-agent and append its
1040
+ // findings — guaranteeing a review stage without relying on the model to
1041
+ // self-delegate one. Opt-in (agentAutoReview); depth-0 only; never fatal.
1042
+ if (!opts.nested
1043
+ && (opts.depth ?? 0) === 0
1044
+ && !opts.dryRun
1045
+ && config.get('agentAutoReview') === true
1046
+ && !opts.abortSignal?.aborted
1047
+ && actions.some(a => a.type === 'write' || a.type === 'edit' || a.type === 'delete')) {
1048
+ try {
1049
+ const reviewTask = `Review the changes just made for this task:\n\n${prompt}\n\nInspect the current state of the changed files (and the git diff). Report concrete issues by severity — correctness/bugs, security, then design — with file:line and a one-line fix each. If it's solid, say so briefly.`;
1050
+ const review = await runDelegate({
1051
+ id: 'auto-review',
1052
+ tool: 'delegate',
1053
+ parameters: { agent: 'reviewer', task: reviewTask },
1054
+ });
1055
+ const body = (review.output || '').replace(/^\[reviewer\]\s*/, '').trim();
1056
+ if (body)
1057
+ finalResponse += `\n\n---\n### Auto-review (reviewer)\n${body}`;
1058
+ }
1059
+ catch {
1060
+ // A failed review must never fail the run.
1061
+ }
1062
+ }
879
1063
  result = {
880
1064
  success: true,
881
1065
  iterations: iteration,
882
1066
  actions,
883
1067
  finalResponse,
884
1068
  };
885
- writeProgressLog(projectContext.root || '', prompt, result, projectContext.name);
1069
+ if (!opts.nested)
1070
+ writeProgressLog(projectContext.root || '', prompt, result, projectContext.name);
886
1071
  return result;
887
1072
  }
888
1073
  catch (error) {
@@ -897,8 +1082,10 @@ export async function runAgent(prompt, projectContext, options = {}) {
897
1082
  return result;
898
1083
  }
899
1084
  finally {
900
- // End session and save history
901
- endSession();
1085
+ // End session and save history. Skipped for nested runs so we don't write
1086
+ // a separate session file or null out the parent's open session.
1087
+ if (!opts.nested)
1088
+ endSession();
902
1089
  }
903
1090
  }
904
1091
  /**
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Sub-agents — named, scoped agent definitions the main ("orchestrator") agent
3
+ * can delegate work to via the `delegate` tool. Each runs as a NESTED agent
4
+ * loop with its own fresh context window, an optional tool allowlist, an
5
+ * optional model override, and a role system prompt — then returns only its
6
+ * final summary to the parent. This keeps the parent's context small and lets
7
+ * each sub-task run with a specialist persona.
8
+ *
9
+ * Storage (mirrors personalities/skills):
10
+ * - **Built-in**: hardcoded below (researcher, reviewer, tester).
11
+ * - **Project**: `<workspace>/.codeep/agents/<name>.md`
12
+ * - **Global**: `~/.codeep/agents/<name>.md`
13
+ * Project shadows global shadows built-in, by name.
14
+ *
15
+ * File format — YAML-ish frontmatter + Markdown body (the role prompt):
16
+ * ```
17
+ * ---
18
+ * name: reviewer
19
+ * description: Reviews a diff for correctness & security
20
+ * tools: [read_file, search_code, execute_command] # allowlist; omit = all
21
+ * model: glm-5.1 # optional provider/model or model override
22
+ * personality: security # optional — reuse a personality preset
23
+ * maxIterations: 15 # optional budget
24
+ * ---
25
+ * You are a senior reviewer. Find correctness & security issues…
26
+ * ```
27
+ */
28
+ export type AgentScope = 'builtin' | 'project' | 'global';
29
+ export interface AgentDef {
30
+ /** Slug (filename without .md, or built-in id). Lowercase, hyphens. */
31
+ name: string;
32
+ /** Human display label. */
33
+ displayName: string;
34
+ /** One-line description shown in the catalog + `/agents`. */
35
+ description: string;
36
+ /** Markdown body — the role system prompt for the sub-agent. */
37
+ prompt: string;
38
+ /** Tool allowlist. Undefined = inherit all of the parent's tools. */
39
+ tools?: string[];
40
+ /** Optional model override ("provider/model" or just "model"). */
41
+ model?: string;
42
+ /** Optional personality preset to layer on (by name). */
43
+ personality?: string;
44
+ /** Optional per-run iteration budget. */
45
+ maxIterations?: number;
46
+ scope: AgentScope;
47
+ }
48
+ export declare function loadAgents(workspaceRoot?: string): AgentDef[];
49
+ export declare function findAgent(name: string, workspaceRoot?: string): AgentDef | null;
50
+ /**
51
+ * The catalog block appended to the orchestrator's system prompt so the model
52
+ * knows which sub-agents it can `delegate` to. Empty string is never returned
53
+ * (built-ins always exist), but callers can choose not to inject it.
54
+ */
55
+ export declare function formatAgentsForSysprompt(agents: AgentDef[]): string;
56
+ /** `/agents` list view (mirrors formatPersonalityList). */
57
+ export declare function formatAgentList(workspaceRoot?: string): string;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Sub-agents — named, scoped agent definitions the main ("orchestrator") agent
3
+ * can delegate work to via the `delegate` tool. Each runs as a NESTED agent
4
+ * loop with its own fresh context window, an optional tool allowlist, an
5
+ * optional model override, and a role system prompt — then returns only its
6
+ * final summary to the parent. This keeps the parent's context small and lets
7
+ * each sub-task run with a specialist persona.
8
+ *
9
+ * Storage (mirrors personalities/skills):
10
+ * - **Built-in**: hardcoded below (researcher, reviewer, tester).
11
+ * - **Project**: `<workspace>/.codeep/agents/<name>.md`
12
+ * - **Global**: `~/.codeep/agents/<name>.md`
13
+ * Project shadows global shadows built-in, by name.
14
+ *
15
+ * File format — YAML-ish frontmatter + Markdown body (the role prompt):
16
+ * ```
17
+ * ---
18
+ * name: reviewer
19
+ * description: Reviews a diff for correctness & security
20
+ * tools: [read_file, search_code, execute_command] # allowlist; omit = all
21
+ * model: glm-5.1 # optional provider/model or model override
22
+ * personality: security # optional — reuse a personality preset
23
+ * maxIterations: 15 # optional budget
24
+ * ---
25
+ * You are a senior reviewer. Find correctness & security issues…
26
+ * ```
27
+ */
28
+ import { readFileSync, readdirSync, existsSync } from 'fs';
29
+ import { join } from 'path';
30
+ import { homedir } from 'os';
31
+ const BUILTIN = [
32
+ {
33
+ name: 'planner',
34
+ displayName: 'Planner',
35
+ description: 'Read-only planner — investigates, then returns a concrete step-by-step implementation plan.',
36
+ scope: 'builtin',
37
+ tools: ['read_file', 'search_code', 'list_files', 'find_files'],
38
+ prompt: `You are a planning sub-agent. Investigate, then produce a plan — do NOT write code or run commands.
39
+ - Read the relevant files to ground the plan in how the code actually works.
40
+ - Return a concise, numbered, step-by-step plan: each step names the file(s) to touch and what changes.
41
+ - Call out risks, assumptions, and anything the implementer must verify.
42
+ - Keep it actionable — the implementer will follow it directly. No code, just the plan.`,
43
+ },
44
+ {
45
+ name: 'researcher',
46
+ displayName: 'Researcher',
47
+ description: 'Read-only explorer — digs through the codebase / web and returns a tight summary.',
48
+ scope: 'builtin',
49
+ tools: ['read_file', 'search_code', 'list_files', 'find_files', 'web_search', 'web_read', 'fetch_url'],
50
+ prompt: `You are a research sub-agent. Your job is to investigate and report — never modify anything.
51
+ - Explore the codebase (and the web when relevant) to answer the task precisely.
52
+ - You CANNOT write or edit files or run commands — read and search only.
53
+ - Return a tight, structured summary: the answer first, then the specific files/lines/sources that back it up.
54
+ - Omit dead ends. The caller only sees your final message, so make it self-contained.`,
55
+ },
56
+ {
57
+ name: 'reviewer',
58
+ displayName: 'Reviewer',
59
+ description: 'Read-only senior review — finds correctness, security, and design issues.',
60
+ scope: 'builtin',
61
+ tools: ['read_file', 'search_code', 'list_files', 'find_files', 'execute_command'],
62
+ personality: 'security',
63
+ prompt: `You are a senior code-review sub-agent. Review only — do not change code.
64
+ - Read the relevant files (and run read-only git/inspection commands) to understand the change in context.
65
+ - Report concrete issues grouped by severity: correctness/bugs, security, then design/naming/tests.
66
+ - Cite file:line for each finding and suggest the fix in one sentence.
67
+ - If it's solid, say so briefly — don't invent problems.`,
68
+ },
69
+ {
70
+ name: 'tester',
71
+ displayName: 'Tester',
72
+ description: 'Writes and runs tests for a target, then reports pass/fail.',
73
+ scope: 'builtin',
74
+ tools: ['read_file', 'write_file', 'edit_file', 'search_code', 'list_files', 'find_files', 'execute_command'],
75
+ prompt: `You are a testing sub-agent. Write focused tests for the target and run them.
76
+ - Match the project's existing test framework and conventions (look at neighbouring tests first).
77
+ - Cover the happy path plus the obvious edge cases; don't over-test.
78
+ - Run the tests and iterate until they pass (or you've found a real bug — then report it).
79
+ - Final message: what you added, the command to run them, and the pass/fail result.`,
80
+ },
81
+ ];
82
+ /** Parse `tools: [a, b]` or `tools: a, b` out of a frontmatter line value. */
83
+ function parseToolsValue(raw) {
84
+ const inner = raw.trim().replace(/^\[/, '').replace(/\]$/, '');
85
+ const list = inner.split(',').map((s) => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
86
+ return list.length > 0 ? list : undefined;
87
+ }
88
+ /** Load custom agents from a `.codeep/agents/` directory. */
89
+ function loadFromDir(dir, scope) {
90
+ if (!existsSync(dir))
91
+ return [];
92
+ const out = [];
93
+ let entries;
94
+ try {
95
+ entries = readdirSync(dir);
96
+ }
97
+ catch {
98
+ return [];
99
+ }
100
+ for (const entry of entries) {
101
+ if (!entry.endsWith('.md'))
102
+ continue;
103
+ const slug = entry.slice(0, -3).toLowerCase();
104
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(slug))
105
+ continue;
106
+ try {
107
+ const raw = readFileSync(join(dir, entry), 'utf8');
108
+ if (raw.length > 64 * 1024)
109
+ continue;
110
+ const fm = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
111
+ const meta = {};
112
+ let body = raw;
113
+ if (fm) {
114
+ body = fm[2];
115
+ for (const line of fm[1].split('\n')) {
116
+ const m = line.match(/^([a-zA-Z]+):\s*(.*)$/);
117
+ if (m)
118
+ meta[m[1].toLowerCase()] = m[2].trim();
119
+ }
120
+ }
121
+ const displayName = meta.name || slug;
122
+ const description = meta.description || `Custom agent from ${entry}`;
123
+ const tools = meta.tools ? parseToolsValue(meta.tools) : undefined;
124
+ const maxIterations = meta.maxiterations ? parseInt(meta.maxiterations, 10) : undefined;
125
+ out.push({
126
+ name: slug,
127
+ displayName,
128
+ description: description.length > 200 ? description.slice(0, 197) + '…' : description,
129
+ prompt: body.trim(),
130
+ tools,
131
+ model: meta.model || undefined,
132
+ personality: meta.personality || undefined,
133
+ maxIterations: Number.isFinite(maxIterations) ? maxIterations : undefined,
134
+ scope,
135
+ });
136
+ }
137
+ catch {
138
+ // Skip broken files — never crash agent loading.
139
+ }
140
+ }
141
+ return out;
142
+ }
143
+ export function loadAgents(workspaceRoot) {
144
+ const project = workspaceRoot ? loadFromDir(join(workspaceRoot, '.codeep', 'agents'), 'project') : [];
145
+ const global = loadFromDir(join(homedir(), '.codeep', 'agents'), 'global');
146
+ const byName = new Map();
147
+ for (const a of BUILTIN)
148
+ byName.set(a.name, a);
149
+ for (const a of global)
150
+ byName.set(a.name, a);
151
+ for (const a of project)
152
+ byName.set(a.name, a);
153
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
154
+ }
155
+ export function findAgent(name, workspaceRoot) {
156
+ const lower = name.toLowerCase();
157
+ return loadAgents(workspaceRoot).find((a) => a.name === lower) ?? null;
158
+ }
159
+ /**
160
+ * The catalog block appended to the orchestrator's system prompt so the model
161
+ * knows which sub-agents it can `delegate` to. Empty string is never returned
162
+ * (built-ins always exist), but callers can choose not to inject it.
163
+ */
164
+ export function formatAgentsForSysprompt(agents) {
165
+ if (agents.length === 0)
166
+ return '';
167
+ const lines = [
168
+ '\n\n## Sub-agents (delegation)',
169
+ 'You can delegate a self-contained sub-task to a specialist sub-agent with the `delegate` tool. It runs in its own fresh context and returns only a summary — use it to keep your own context focused (e.g. send a researcher to explore, a reviewer to critique, a tester to write tests). Available agents:',
170
+ '',
171
+ ];
172
+ for (const a of agents)
173
+ lines.push(`- \`${a.name}\` — ${a.description}`);
174
+ lines.push('', 'Call `delegate({ "agent": "<name>", "task": "<clear, self-contained instruction>" })`. Omit `agent` for a general-purpose sub-agent. Do the work yourself for small/quick tasks — delegation has overhead.');
175
+ return lines.join('\n');
176
+ }
177
+ /** `/agents` list view (mirrors formatPersonalityList). */
178
+ export function formatAgentList(workspaceRoot) {
179
+ const list = loadAgents(workspaceRoot);
180
+ const lines = ['## Sub-agents', '', 'The agent can `delegate` self-contained sub-tasks to these. Each runs in its own context and returns a summary.', '', '| Name | Scope | Tools | Description |', '|---|---|---|---|'];
181
+ for (const a of list) {
182
+ const tag = a.scope === 'builtin' ? 'built-in' : a.scope;
183
+ const tools = a.tools ? `${a.tools.length} scoped` : 'all';
184
+ lines.push(`| \`${a.name}\` | ${tag} | ${tools} | ${a.description} |`);
185
+ }
186
+ lines.push('', 'Add your own: drop a `<name>.md` with frontmatter (name, description, tools, model, personality) in `.codeep/agents/` (project) or `~/.codeep/agents/` (global).');
187
+ return lines.join('\n');
188
+ }
@@ -110,4 +110,9 @@ export declare function pullLearning(): Promise<{
110
110
  } | null>;
111
111
  export declare function pushProfiles(profiles: Record<string, object>): Promise<boolean>;
112
112
  export declare function pullProfiles(): Promise<Record<string, object> | null>;
113
+ /** Push the local global profile.md to the dashboard. */
114
+ export declare function pushUserProfile(): Promise<boolean>;
115
+ /** Pull the dashboard profile.md — additive: writes only when no local profile
116
+ * exists. Returns 1 if written, 0 if skipped, null on error / not linked. */
117
+ export declare function pullUserProfile(): Promise<number | null>;
113
118
  export declare function syncMemoryNotes(projectName: string, notes: string[]): Promise<void>;
@@ -421,6 +421,64 @@ export async function pullProfiles() {
421
421
  return null;
422
422
  }
423
423
  }
424
+ // ─── User profile sync (~/.codeep/profile.md) ──────────────────────────────────
425
+ //
426
+ // The hand-written global "About me" profile. One blob per user. Pull is
427
+ // additive — it writes only when no local profile.md exists, so a web edit can
428
+ // never clobber local work (same philosophy as the personalities/commands sync).
429
+ function userProfilePath() {
430
+ return join(homedir(), '.codeep', 'profile.md');
431
+ }
432
+ /** Push the local global profile.md to the dashboard. */
433
+ export async function pushUserProfile() {
434
+ const syncToken = getSyncToken();
435
+ if (!syncToken)
436
+ return false;
437
+ const path = userProfilePath();
438
+ if (!existsSync(path))
439
+ return false;
440
+ let content = '';
441
+ try {
442
+ content = readFileSync(path, 'utf8');
443
+ }
444
+ catch {
445
+ return false;
446
+ }
447
+ if (content.length > 32 * 1024)
448
+ content = content.slice(0, 32 * 1024);
449
+ const res = await fetchWithRetry(`${API_BASE}/api/sync/user-profile`, {
450
+ method: 'POST',
451
+ headers: { 'Content-Type': 'application/json', 'x-sync-token': syncToken },
452
+ body: JSON.stringify({ content }),
453
+ });
454
+ return res?.ok ?? false;
455
+ }
456
+ /** Pull the dashboard profile.md — additive: writes only when no local profile
457
+ * exists. Returns 1 if written, 0 if skipped, null on error / not linked. */
458
+ export async function pullUserProfile() {
459
+ const syncToken = getSyncToken();
460
+ if (!syncToken)
461
+ return null;
462
+ const res = await fetchWithRetry(`${API_BASE}/api/sync/user-profile`, { headers: { 'x-sync-token': syncToken } });
463
+ if (!res?.ok)
464
+ return null;
465
+ try {
466
+ const data = await res.json();
467
+ if (!data.ok || !data.content)
468
+ return 0;
469
+ const path = userProfilePath();
470
+ if (existsSync(path))
471
+ return 0; // never clobber local
472
+ const dir = join(homedir(), '.codeep');
473
+ if (!existsSync(dir))
474
+ mkdirSync(dir, { recursive: true });
475
+ writeFileSync(path, data.content);
476
+ return 1;
477
+ }
478
+ catch {
479
+ return null;
480
+ }
481
+ }
424
482
  export async function syncMemoryNotes(projectName, notes) {
425
483
  const syncToken = getSyncToken();
426
484
  if (!syncToken)