bloby-bot 0.19.2 → 0.20.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.
@@ -14,6 +14,9 @@ import { closeDb, getSession, getSetting } from '../worker/db.js';
14
14
  import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, isBackendStopping, resetBackendRestarts } from './backend.js';
15
15
  import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
16
16
  import { startBlobyAgentQuery, stopBlobyAgentQuery, type RecentMessage } from './bloby-agent.js';
17
+ import { parseDelegateBlocks } from './delegate-parser.js';
18
+ import { spawnSubAgent } from './sub-agent.js';
19
+ import { stopTask, stopAllTasks } from './task-board.js';
17
20
  import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
18
21
  import { startViteDevServers, stopViteDevServers } from './vite-dev.js';
19
22
  import { startScheduler, stopScheduler } from './scheduler.js';
@@ -1132,7 +1135,12 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1132
1135
  const waMirrorJid = waStatus?.connected ? waStatus.info?.phoneNumber : null;
1133
1136
  let waChunkBuf = '';
1134
1137
 
1135
- // Start agent query
1138
+ // Start orchestrator query (maxTurns: 5 — fast, delegates heavy work)
1139
+ log.info(`[orchestrator] ──── USER MESSAGE ────`);
1140
+ log.info(`[orchestrator] Content: "${content.slice(0, 100)}..."`);
1141
+ log.info(`[orchestrator] Model: ${freshConfig.ai.model}`);
1142
+ log.info(`[orchestrator] Conv: ${convId}`);
1143
+ log.info(`[orchestrator] MaxTurns: 5 (orchestrator mode)`);
1136
1144
  agentQueryActive = true;
1137
1145
  currentStreamConvId = convId;
1138
1146
  currentStreamBuffer = '';
@@ -1149,8 +1157,10 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1149
1157
  waChunkBuf = '';
1150
1158
  }
1151
1159
 
1152
- // Intercept bot:done — Vite HMR handles file changes automatically
1160
+ // Intercept bot:done — orchestrator turn finished
1153
1161
  if (type === 'bot:done') {
1162
+ log.info(`[orchestrator] ──── TURN COMPLETE ────`);
1163
+ log.info(`[orchestrator] File tools used: ${eventData.usedFileTools}`);
1154
1164
  agentQueryActive = false;
1155
1165
  currentStreamConvId = null;
1156
1166
  currentStreamBuffer = '';
@@ -1180,20 +1190,55 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1180
1190
  waChunkBuf = '';
1181
1191
  }
1182
1192
 
1193
+ // Parse <Delegate> blocks — spawn sub-agents, strip from visible text
1194
+ log.info(`[orchestrator] Parsing response for <Delegate> blocks...`);
1195
+ const { blocks, cleanText } = parseDelegateBlocks(eventData.content || '');
1196
+
1197
+ if (blocks.length) {
1198
+ log.info(`[orchestrator] Found ${blocks.length} delegation(s) — spawning sub-agent(s)`);
1199
+ } else {
1200
+ log.info(`[orchestrator] No delegations — direct response`);
1201
+ }
1202
+
1203
+ for (const block of blocks) {
1204
+ log.info(`[orchestrator] Spawning sub-agent: type=${block.type}, task="${block.description.slice(0, 80)}..."`);
1205
+ spawnSubAgent({
1206
+ type: block.type,
1207
+ description: block.description,
1208
+ conversationId: convId,
1209
+ model: freshConfig.ai.model,
1210
+ names: { botName, humanName },
1211
+ broadcastBloby,
1212
+ restartBackend: () => {
1213
+ resetBackendRestarts();
1214
+ stopBackend().then(() => spawnBackend(backendPort));
1215
+ },
1216
+ });
1217
+ }
1218
+
1219
+ const displayContent = cleanText;
1220
+
1183
1221
  (async () => {
1184
1222
  try {
1185
1223
  await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
1186
- role: 'assistant', content: eventData.content, meta: { model: freshConfig.ai.model },
1224
+ role: 'assistant', content: displayContent, meta: { model: freshConfig.ai.model },
1187
1225
  });
1188
1226
  } catch (err: any) {
1189
1227
  log.warn(`[bloby] DB persist bot response error: ${err.message}`);
1190
1228
  }
1191
1229
  })();
1230
+
1231
+ // Broadcast cleaned text (without <Delegate> blocks)
1232
+ broadcastBloby('bot:response', { ...eventData, content: displayContent });
1233
+ return; // prevent double-broadcast below
1192
1234
  }
1193
1235
 
1194
- // Stream all events to every connected client
1236
+ // Stream all other events to every connected client
1195
1237
  broadcastBloby(type, eventData);
1196
- }, data.attachments, savedFiles, { botName, humanName }, recentMessages);
1238
+ }, data.attachments, savedFiles, { botName, humanName }, recentMessages,
1239
+ undefined, // no supportPrompt
1240
+ 5, // maxTurns: orchestrator mode
1241
+ );
1197
1242
  })();
1198
1243
  return;
1199
1244
  }
@@ -1231,6 +1276,23 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1231
1276
  return;
1232
1277
  }
1233
1278
 
1279
+ if (msg.type === 'user:stop-task') {
1280
+ const taskId = (msg as any).data?.taskId;
1281
+ if (taskId) {
1282
+ log.info(`[orchestrator] Stopping task: ${taskId}`);
1283
+ stopTask(taskId);
1284
+ broadcastBloby('bot:task-stopped', { taskId });
1285
+ }
1286
+ return;
1287
+ }
1288
+
1289
+ if (msg.type === 'user:stop-all-tasks') {
1290
+ log.info(`[orchestrator] Stopping all background tasks`);
1291
+ stopAllTasks();
1292
+ broadcastBloby('bot:tasks-cleared', {});
1293
+ return;
1294
+ }
1295
+
1234
1296
  if (msg.type === 'user:clear-context') {
1235
1297
  (async () => {
1236
1298
  try {
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Sub-Agent Spawner — creates and manages background agent queries.
3
+ * Sub-agents run independently from the orchestrator, handling
4
+ * heavy work (coding, research) while the user keeps chatting.
5
+ */
6
+
7
+ import { log } from '../shared/logger.js';
8
+ import { startBlobyAgentQuery } from './bloby-agent.js';
9
+ import { createTask, completeTask, failTask, setOnTaskPromoted, type SubAgentTask, type SubAgentType } from './task-board.js';
10
+ import { assembleSubAgentPrompt } from '../worker/prompts/sub-agent-prompts.js';
11
+
12
+ export interface SpawnOpts {
13
+ type: SubAgentType;
14
+ description: string;
15
+ conversationId: string;
16
+ model: string;
17
+ names: { botName: string; humanName: string };
18
+ broadcastBloby: (type: string, data: any) => void;
19
+ restartBackend: () => void;
20
+ }
21
+
22
+ /** Pending spawn options for queued tasks, keyed by task ID */
23
+ const pendingOpts = new Map<string, SpawnOpts>();
24
+
25
+ // Register the global promotion handler once (handles all queued tasks)
26
+ setOnTaskPromoted((task: SubAgentTask) => {
27
+ const opts = pendingOpts.get(task.id);
28
+ if (opts) {
29
+ pendingOpts.delete(task.id);
30
+ log.info(`[sub-agent] Queued task ${task.id} promoted — starting agent`);
31
+ opts.broadcastBloby('bot:task-progress', {
32
+ taskId: task.id,
33
+ tool: 'starting',
34
+ });
35
+ runSubAgent(task, opts);
36
+ }
37
+ });
38
+
39
+ /**
40
+ * Spawn a background sub-agent for a delegated task.
41
+ * Returns the created task, or null if the queue is full.
42
+ */
43
+ export function spawnSubAgent(opts: SpawnOpts): SubAgentTask | null {
44
+ log.info(`[sub-agent] ──── SPAWN REQUEST ────`);
45
+ log.info(`[sub-agent] Type: ${opts.type}`);
46
+ log.info(`[sub-agent] Description: "${opts.description.slice(0, 120)}..."`);
47
+ log.info(`[sub-agent] Model: ${opts.model}`);
48
+ log.info(`[sub-agent] Conversation: ${opts.conversationId}`);
49
+
50
+ const task = createTask(opts.type, opts.description, opts.conversationId);
51
+ if (!task) {
52
+ log.warn(`[sub-agent] Could not create task — queue full`);
53
+ opts.broadcastBloby('bot:task-error', {
54
+ error: 'Too many background tasks. Try again in a moment.',
55
+ });
56
+ return null;
57
+ }
58
+
59
+ // Broadcast task creation
60
+ opts.broadcastBloby('bot:task-created', {
61
+ taskId: task.id,
62
+ type: task.type,
63
+ description: task.description,
64
+ status: task.status,
65
+ });
66
+
67
+ if (task.status === 'queued') {
68
+ // Store opts so we can start the agent when promoted
69
+ pendingOpts.set(task.id, opts);
70
+ log.info(`[sub-agent] Task ${task.id} queued — will start when a slot opens`);
71
+ return task;
72
+ }
73
+
74
+ // Task is immediately running
75
+ log.info(`[sub-agent] Task ${task.id} starting immediately (slot available)`);
76
+ runSubAgent(task, opts);
77
+ return task;
78
+ }
79
+
80
+ /** Actually start the agent query for a task */
81
+ function runSubAgent(task: SubAgentTask, opts: SpawnOpts): void {
82
+ const systemPrompt = assembleSubAgentPrompt(opts.type, opts.names);
83
+ const convId = `subagent-${task.id}`;
84
+
85
+ log.info(`[sub-agent] ──── STARTING AGENT ────`);
86
+ log.info(`[sub-agent] Task ID: ${task.id}`);
87
+ log.info(`[sub-agent] Type: ${task.type}`);
88
+ log.info(`[sub-agent] Conv ID: ${convId}`);
89
+ log.info(`[sub-agent] Prompt length: ${systemPrompt.length} chars`);
90
+ log.info(`[sub-agent] Task description: "${task.description.slice(0, 120)}..."`);
91
+
92
+ let fullText = '';
93
+ let toolCount = 0;
94
+
95
+ startBlobyAgentQuery(
96
+ convId,
97
+ task.description,
98
+ opts.model,
99
+ (type, eventData) => {
100
+ // Track tool usage for progress
101
+ if (type === 'bot:tool') {
102
+ toolCount++;
103
+ const toolName = eventData.name || 'working';
104
+ task.progress.push(toolName);
105
+ if (toolName === 'Write' || toolName === 'Edit') {
106
+ task.usedFileTools = true;
107
+ }
108
+ log.info(`[sub-agent] Task ${task.id} | Tool #${toolCount}: ${toolName}${eventData.input?.file_path ? ` → ${eventData.input.file_path}` : ''}`);
109
+ opts.broadcastBloby('bot:task-progress', {
110
+ taskId: task.id,
111
+ tool: toolName,
112
+ input: eventData.input,
113
+ });
114
+ }
115
+
116
+ // Accumulate response text
117
+ if (type === 'bot:token' && eventData.token) {
118
+ fullText += eventData.token;
119
+ }
120
+
121
+ // Store final response
122
+ if (type === 'bot:response') {
123
+ fullText = eventData.content || fullText;
124
+ log.info(`[sub-agent] Task ${task.id} | Response received (${fullText.length} chars)`);
125
+ }
126
+
127
+ // Handle completion
128
+ if (type === 'bot:done') {
129
+ const usedFiles = task.usedFileTools || eventData.usedFileTools;
130
+ const elapsed = Math.round((Date.now() - task.createdAt) / 1000);
131
+ completeTask(task.id, fullText || '(completed)', usedFiles);
132
+
133
+ log.info(`[sub-agent] ──── TASK COMPLETED ────`);
134
+ log.info(`[sub-agent] Task ID: ${task.id}`);
135
+ log.info(`[sub-agent] Duration: ${elapsed}s`);
136
+ log.info(`[sub-agent] Tools used: ${toolCount}`);
137
+ log.info(`[sub-agent] File tools: ${usedFiles}`);
138
+ log.info(`[sub-agent] Result preview: "${(fullText || '').slice(0, 200)}..."`);
139
+
140
+ opts.broadcastBloby('bot:task-done', {
141
+ taskId: task.id,
142
+ type: task.type,
143
+ description: task.description,
144
+ result: fullText?.slice(0, 500) || '(completed)',
145
+ usedFileTools: usedFiles,
146
+ });
147
+
148
+ // Restart backend if file tools were used
149
+ if (usedFiles) {
150
+ log.info(`[sub-agent] Task ${task.id} used file tools — restarting backend`);
151
+ opts.restartBackend();
152
+ }
153
+ }
154
+
155
+ // Handle errors
156
+ if (type === 'bot:error') {
157
+ const errorMsg = eventData.error || 'Unknown error';
158
+ failTask(task.id, errorMsg);
159
+ opts.broadcastBloby('bot:task-error', {
160
+ taskId: task.id,
161
+ error: errorMsg,
162
+ });
163
+ log.warn(`[sub-agent] ──── TASK FAILED ────`);
164
+ log.warn(`[sub-agent] Task ID: ${task.id}`);
165
+ log.warn(`[sub-agent] Error: ${errorMsg}`);
166
+ }
167
+ },
168
+ undefined, // no attachments
169
+ undefined, // no savedFiles
170
+ opts.names,
171
+ undefined, // no recentMessages — sub-agents don't need chat history
172
+ systemPrompt, // passed as supportPrompt to bypass main prompt assembly
173
+ 50, // maxTurns — full power for sub-agents
174
+ );
175
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Task Board — tracks background sub-agent tasks.
3
+ * In-memory only (no DB persistence). Tasks are ephemeral — they exist
4
+ * for the duration of the work and a short window after for reporting.
5
+ */
6
+
7
+ import { log } from '../shared/logger.js';
8
+
9
+ export type SubAgentType = 'coder' | 'researcher';
10
+
11
+ export interface SubAgentTask {
12
+ id: string;
13
+ type: SubAgentType;
14
+ description: string;
15
+ status: 'queued' | 'running' | 'done' | 'error';
16
+ progress: string[];
17
+ result?: string;
18
+ error?: string;
19
+ usedFileTools: boolean;
20
+ createdAt: number;
21
+ completedAt?: number;
22
+ conversationId: string;
23
+ abortController: AbortController;
24
+ }
25
+
26
+ const tasks = new Map<string, SubAgentTask>();
27
+ const taskQueue: SubAgentTask[] = [];
28
+ const MAX_CONCURRENT = 3;
29
+ const MAX_QUEUED = 5;
30
+ /** Keep completed tasks for 5 minutes before cleanup */
31
+ const COMPLETED_TTL = 5 * 60 * 1000;
32
+
33
+ /** Callback invoked when a queued task gets promoted to running */
34
+ let onTaskPromoted: ((task: SubAgentTask) => void) | null = null;
35
+
36
+ /** Register a callback for when queued tasks are promoted */
37
+ export function setOnTaskPromoted(cb: (task: SubAgentTask) => void) {
38
+ onTaskPromoted = cb;
39
+ }
40
+
41
+ function generateId(): string {
42
+ return 'task-' + Math.random().toString(36).slice(2, 8) + Date.now().toString(36).slice(-4);
43
+ }
44
+
45
+ function getRunningCount(): number {
46
+ let count = 0;
47
+ for (const t of tasks.values()) {
48
+ if (t.status === 'running') count++;
49
+ }
50
+ return count;
51
+ }
52
+
53
+ /** Create a task. Returns the task (queued or running), or null if queue is full. */
54
+ export function createTask(
55
+ type: SubAgentType,
56
+ description: string,
57
+ conversationId: string,
58
+ ): SubAgentTask | null {
59
+ const task: SubAgentTask = {
60
+ id: generateId(),
61
+ type,
62
+ description,
63
+ status: 'queued',
64
+ progress: [],
65
+ usedFileTools: false,
66
+ createdAt: Date.now(),
67
+ conversationId,
68
+ abortController: new AbortController(),
69
+ };
70
+
71
+ if (getRunningCount() < MAX_CONCURRENT) {
72
+ task.status = 'running';
73
+ tasks.set(task.id, task);
74
+ log.info(`[task-board] Created task ${task.id} (${type}) — running`);
75
+ return task;
76
+ }
77
+
78
+ if (taskQueue.length >= MAX_QUEUED) {
79
+ log.warn(`[task-board] Queue full (${MAX_QUEUED}) — rejecting task`);
80
+ return null;
81
+ }
82
+
83
+ tasks.set(task.id, task);
84
+ taskQueue.push(task);
85
+ log.info(`[task-board] Created task ${task.id} (${type}) — queued (position ${taskQueue.length})`);
86
+ return task;
87
+ }
88
+
89
+ /** Mark a task as completed */
90
+ export function completeTask(id: string, result: string, usedFileTools: boolean): void {
91
+ const task = tasks.get(id);
92
+ if (!task) return;
93
+ task.status = 'done';
94
+ task.result = result;
95
+ task.usedFileTools = usedFileTools;
96
+ task.completedAt = Date.now();
97
+ log.info(`[task-board] Task ${id} completed`);
98
+ promoteNext();
99
+ scheduleCleanup(id);
100
+ }
101
+
102
+ /** Mark a task as failed */
103
+ export function failTask(id: string, error: string): void {
104
+ const task = tasks.get(id);
105
+ if (!task) return;
106
+ task.status = 'error';
107
+ task.error = error;
108
+ task.completedAt = Date.now();
109
+ log.warn(`[task-board] Task ${id} failed: ${error}`);
110
+ promoteNext();
111
+ scheduleCleanup(id);
112
+ }
113
+
114
+ /** Stop a task by aborting its controller */
115
+ export function stopTask(id: string): boolean {
116
+ const task = tasks.get(id);
117
+ if (!task) return false;
118
+
119
+ // If queued, just remove from queue
120
+ if (task.status === 'queued') {
121
+ const idx = taskQueue.indexOf(task);
122
+ if (idx !== -1) taskQueue.splice(idx, 1);
123
+ tasks.delete(id);
124
+ log.info(`[task-board] Removed queued task ${id}`);
125
+ return true;
126
+ }
127
+
128
+ if (task.status === 'running') {
129
+ task.abortController.abort();
130
+ task.status = 'error';
131
+ task.error = 'Stopped by user';
132
+ task.completedAt = Date.now();
133
+ log.info(`[task-board] Stopped task ${id}`);
134
+ promoteNext();
135
+ scheduleCleanup(id);
136
+ return true;
137
+ }
138
+
139
+ return false;
140
+ }
141
+
142
+ /** Stop all active tasks */
143
+ export function stopAllTasks(): void {
144
+ for (const task of tasks.values()) {
145
+ if (task.status === 'running') {
146
+ task.abortController.abort();
147
+ task.status = 'error';
148
+ task.error = 'Stopped by user';
149
+ task.completedAt = Date.now();
150
+ }
151
+ }
152
+ // Clear queue
153
+ while (taskQueue.length) {
154
+ const t = taskQueue.pop()!;
155
+ tasks.delete(t.id);
156
+ }
157
+ log.info('[task-board] All tasks stopped');
158
+ }
159
+
160
+ /** Get all active (running + queued) tasks */
161
+ export function getActiveTasks(): SubAgentTask[] {
162
+ return Array.from(tasks.values()).filter(
163
+ (t) => t.status === 'running' || t.status === 'queued',
164
+ );
165
+ }
166
+
167
+ /** Get recently completed/failed tasks (within TTL window) */
168
+ export function getRecentCompletedTasks(): SubAgentTask[] {
169
+ const cutoff = Date.now() - COMPLETED_TTL;
170
+ return Array.from(tasks.values()).filter(
171
+ (t) => (t.status === 'done' || t.status === 'error') && (t.completedAt || 0) > cutoff,
172
+ );
173
+ }
174
+
175
+ /** Format the task board as markdown for system prompt injection */
176
+ export function formatTaskBoard(): string {
177
+ const active = getActiveTasks();
178
+ const completed = getRecentCompletedTasks();
179
+
180
+ if (!active.length && !completed.length) return '';
181
+
182
+ const lines: string[] = [];
183
+
184
+ if (active.length) {
185
+ lines.push('## Active Tasks');
186
+ for (const t of active) {
187
+ const elapsed = Math.round((Date.now() - t.createdAt) / 1000);
188
+ const time = elapsed < 60 ? `${elapsed}s ago` : `${Math.round(elapsed / 60)}m ago`;
189
+ const lastTool = t.progress.length ? t.progress[t.progress.length - 1] : 'starting';
190
+ lines.push(`- **${t.id}** (${t.type}, ${t.status}): "${t.description.slice(0, 100)}"`);
191
+ lines.push(` Started ${time} | Last: ${lastTool}`);
192
+ }
193
+ }
194
+
195
+ if (completed.length) {
196
+ lines.push('');
197
+ lines.push('## Recently Completed Tasks');
198
+ for (const t of completed) {
199
+ const statusLabel = t.status === 'done' ? 'DONE' : `ERROR: ${t.error}`;
200
+ lines.push(`- **${t.id}** (${t.type}, ${statusLabel}): "${t.description.slice(0, 100)}"`);
201
+ if (t.result) {
202
+ lines.push(` Result: ${t.result.slice(0, 300)}`);
203
+ }
204
+ }
205
+ }
206
+
207
+ return lines.join('\n');
208
+ }
209
+
210
+ // ── Internal ──
211
+
212
+ function promoteNext() {
213
+ if (!taskQueue.length || getRunningCount() >= MAX_CONCURRENT) return;
214
+
215
+ const next = taskQueue.shift()!;
216
+ next.status = 'running';
217
+ log.info(`[task-board] Promoted queued task ${next.id} to running`);
218
+
219
+ if (onTaskPromoted) {
220
+ onTaskPromoted(next);
221
+ }
222
+ }
223
+
224
+ function scheduleCleanup(id: string) {
225
+ setTimeout(() => {
226
+ const task = tasks.get(id);
227
+ if (task && (task.status === 'done' || task.status === 'error')) {
228
+ tasks.delete(id);
229
+ }
230
+ }, COMPLETED_TTL);
231
+ }
@@ -204,6 +204,70 @@ Complex cron tasks can have detailed instruction files in `tasks/{cron-id}.md`.
204
204
 
205
205
  ---
206
206
 
207
+ # Delegation
208
+
209
+ You can delegate heavy work to background sub-agents. This lets you respond quickly while work happens in parallel. The user keeps chatting with you while sub-agents build, research, or process in the background.
210
+
211
+ ## When to Delegate
212
+
213
+ **Delegate to a sub-agent** (takes many tool calls, runs in background):
214
+ - Building new features (pages, APIs, components)
215
+ - Multi-file refactoring or code changes
216
+ - Complex bug fixes requiring investigation
217
+ - Any coding task that would take more than 2-3 tool calls
218
+
219
+ **Handle directly** (quick, do it yourself):
220
+ - Conversational responses, chitchat, questions
221
+ - Memory file writes (MEMORY.md, MYSELF.md, MYHUMAN.md, daily notes)
222
+ - Config edits (PULSE.json, CRONS.json, MCP.json)
223
+ - Channel configuration (mode, admins)
224
+ - Simple file reads or status checks
225
+ - Setting up reminders or crons
226
+
227
+ ## How to Delegate
228
+
229
+ Include a `<Delegate>` block in your response. The supervisor strips it from your visible message, spawns a background coding agent, and your human never sees the XML — they just see your conversational response.
230
+
231
+ ```
232
+ <Delegate type="coder">
233
+ Build a contacts page with search and tag filtering.
234
+ - Backend: Express API at /api/contacts (GET, POST, DELETE) using SQLite
235
+ - Frontend: React page at client/src/components/Dashboard/ContactsPage.tsx
236
+ - Add sidebar link and route in App.tsx
237
+ </Delegate>
238
+ ```
239
+
240
+ You can include multiple `<Delegate>` blocks in one response for parallel work. Always write a natural conversational message alongside the delegation — your human should know what you're doing.
241
+
242
+ **Good example:**
243
+ > "On it! I'll build that contacts page with search and tags. Give me a few minutes."
244
+ > `<Delegate type="coder">...</Delegate>`
245
+
246
+ ## Available Sub-Agent Types
247
+
248
+ | Type | Use For |
249
+ |------|---------|
250
+ | `coder` | All coding tasks — features, fixes, refactoring, full-stack development |
251
+
252
+ ## Task Board
253
+
254
+ A "Background Tasks" section appears in your context showing active and recently completed sub-agent tasks. Use it to:
255
+
256
+ - **Report results naturally** when tasks complete: "That contacts page is ready — I added search and tag filtering."
257
+ - **Give progress updates** if the user asks: "Still working on it — currently editing the API routes."
258
+ - **Avoid duplicates** — check if a similar task is already running before delegating again.
259
+
260
+ When a task completes, report the results to your human in your next response. Be specific about what was built or changed.
261
+
262
+ ## Rules
263
+
264
+ - **Max 3 concurrent sub-agents.** If all slots are busy, tasks queue automatically (up to 5 queued).
265
+ - **Keep task descriptions specific and actionable.** Include file paths, technologies, and acceptance criteria when relevant.
266
+ - **Never delegate memory or config edits** — MYSELF.md, MYHUMAN.md, MEMORY.md, PULSE.json, CRONS.json are yours to manage directly.
267
+ - **Always respond conversationally alongside delegation.** Don't just silently delegate — tell your human what you're doing.
268
+
269
+ ---
270
+
207
271
  ## Skills
208
272
 
209
273
  Skills live in `skills/` — each skill is a folder with instructions and resources: