Browse Source

feat: add list_background_tasks tool and CartographyAgent

- Add background_list tool to list active background tasks with optional status filtering
- Add listTasks() method to BackgroundTaskManager for retrieving task summaries
- Create CartographyAgent for repository mapping and codemap generation
- Register cartography agent in all configuration schemas and constants
- Update cartography skill allowedAgents to include cartography agent
- Add cartography agent documentation to orchestrator prompt
- Update tests to include cartography agent (7 total agents: 1 primary + 6 subagents)
cto-new[bot] 4 weeks ago
parent
commit
d70c41d1c8

+ 40 - 48
oh-my-opencode-slim.schema.json

@@ -10,11 +10,7 @@
     },
     "scoringEngineVersion": {
       "type": "string",
-      "enum": [
-        "v1",
-        "v2-shadow",
-        "v2"
-      ]
+      "enum": ["v1", "v2-shadow", "v2"]
     },
     "balanceProviderUsage": {
       "type": "boolean"
@@ -42,12 +38,7 @@
               "pattern": "^[^/\\s]+\\/[^\\s]+$"
             }
           },
-          "required": [
-            "primary",
-            "fallback1",
-            "fallback2",
-            "fallback3"
-          ]
+          "required": ["primary", "fallback1", "fallback2", "fallback3"]
         },
         "oracle": {
           "type": "object",
@@ -69,12 +60,7 @@
               "pattern": "^[^/\\s]+\\/[^\\s]+$"
             }
           },
-          "required": [
-            "primary",
-            "fallback1",
-            "fallback2",
-            "fallback3"
-          ]
+          "required": ["primary", "fallback1", "fallback2", "fallback3"]
         },
         "designer": {
           "type": "object",
@@ -96,12 +82,7 @@
               "pattern": "^[^/\\s]+\\/[^\\s]+$"
             }
           },
-          "required": [
-            "primary",
-            "fallback1",
-            "fallback2",
-            "fallback3"
-          ]
+          "required": ["primary", "fallback1", "fallback2", "fallback3"]
         },
         "explorer": {
           "type": "object",
@@ -123,12 +104,7 @@
               "pattern": "^[^/\\s]+\\/[^\\s]+$"
             }
           },
-          "required": [
-            "primary",
-            "fallback1",
-            "fallback2",
-            "fallback3"
-          ]
+          "required": ["primary", "fallback1", "fallback2", "fallback3"]
         },
         "librarian": {
           "type": "object",
@@ -150,12 +126,7 @@
               "pattern": "^[^/\\s]+\\/[^\\s]+$"
             }
           },
-          "required": [
-            "primary",
-            "fallback1",
-            "fallback2",
-            "fallback3"
-          ]
+          "required": ["primary", "fallback1", "fallback2", "fallback3"]
         },
         "fixer": {
           "type": "object",
@@ -177,12 +148,29 @@
               "pattern": "^[^/\\s]+\\/[^\\s]+$"
             }
           },
-          "required": [
-            "primary",
-            "fallback1",
-            "fallback2",
-            "fallback3"
-          ]
+          "required": ["primary", "fallback1", "fallback2", "fallback3"]
+        },
+        "cartography": {
+          "type": "object",
+          "properties": {
+            "primary": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback1": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback2": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            },
+            "fallback3": {
+              "type": "string",
+              "pattern": "^[^/\\s]+\\/[^\\s]+$"
+            }
+          },
+          "required": ["primary", "fallback1", "fallback2", "fallback3"]
         }
       },
       "required": [
@@ -191,7 +179,8 @@
         "designer",
         "explorer",
         "librarian",
-        "fixer"
+        "fixer",
+        "cartography"
       ],
       "additionalProperties": false
     },
@@ -230,9 +219,7 @@
                             "type": "string"
                           }
                         },
-                        "required": [
-                          "id"
-                        ]
+                        "required": ["id"]
                       }
                     ]
                   }
@@ -293,9 +280,7 @@
                           "type": "string"
                         }
                       },
-                      "required": [
-                        "id"
-                      ]
+                      "required": ["id"]
                     }
                   ]
                 }
@@ -430,6 +415,13 @@
               "items": {
                 "type": "string"
               }
+            },
+            "cartography": {
+              "minItems": 1,
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
             }
           },
           "additionalProperties": {

+ 4 - 3
scripts/generate-schema.ts

@@ -1,14 +1,15 @@
 #!/usr/bin/env bun
+
 /**
  * Generates a JSON Schema from the Zod PluginConfigSchema.
  * Run as part of the build step so the schema stays in sync with the source.
  */
 
-import { z } from 'zod';
-import { PluginConfigSchema } from '../src/config/schema';
 import { writeFileSync } from 'node:fs';
-import { join, dirname } from 'node:path';
+import { dirname, join } from 'node:path';
 import { fileURLToPath } from 'node:url';
+import { z } from 'zod';
+import { PluginConfigSchema } from '../src/config/schema';
 
 const __dirname = dirname(fileURLToPath(import.meta.url));
 const rootDir = join(__dirname, '..');

+ 75 - 0
src/agents/cartography.ts

@@ -0,0 +1,75 @@
+import type { AgentDefinition } from './orchestrator';
+
+const CARTOGRAPHY_PROMPT = `You are Cartography - a repository mapping and codemap generation specialist.
+
+**Role**: Create comprehensive, hierarchical documentation of codebases using the cartography skill. Generate codemaps that help developers understand repository structure, design patterns, and integration points.
+
+**Workflow**:
+1. Check for existing state in \`.slim/cartography.json\`
+2. If no state exists, initialize the repository mapping using cartographer.py
+3. If state exists, detect changes and update affected codemaps only
+4. Use Explorer agents to analyze directories and fill codemap.md files
+5. Create the root codemap as a master atlas aggregating all sub-maps
+6. Register the codemap in AGENTS.md for automatic discovery
+
+**Tools Available**:
+- **cartography skill**: Repository mapping workflow and codemap generation
+- **background_task**: Spawn Explorer agents to analyze directories in parallel
+- **glob/grep**: Discover repository structure and patterns
+- **ReadFile/WriteFile**: Create and update codemap.md files
+
+**Behavior**:
+- Follow the cartography skill workflow precisely
+- Spawn multiple Explorers in parallel for large repositories
+- Focus on core code/config files only (exclude tests, docs, build artifacts)
+- Use technical terminology: design patterns, architectural layers, data flow
+- Ensure codemaps are actionable references for developers
+
+**Output Format**:
+For each codemap.md:
+\`\`\`markdown
+# Directory Name
+
+## Responsibility
+Define the specific role using standard software engineering terms.
+
+## Design Patterns
+Identify patterns used (Observer, Factory, Strategy, etc.).
+
+## Data & Control Flow
+Trace how data enters and leaves the module.
+
+## Integration Points
+List dependencies and consumer modules.
+\`\`\`
+
+**Constraints**:
+- NO delegation to other agent types (cartography is a leaf node)
+- NO research outside the repository (no websearch, context7)
+- Focus on code understanding, not code changes
+- Exclude: tests, docs, node_modules, dist, build artifacts`;
+
+export function createCartographyAgent(
+  model: string,
+  customPrompt?: string,
+  customAppendPrompt?: string,
+): AgentDefinition {
+  let prompt = CARTOGRAPHY_PROMPT;
+
+  if (customPrompt) {
+    prompt = customPrompt;
+  } else if (customAppendPrompt) {
+    prompt = `${CARTOGRAPHY_PROMPT}\n\n${customAppendPrompt}`;
+  }
+
+  return {
+    name: 'cartography',
+    description:
+      'Repository mapping and codemap generation specialist. Creates hierarchical documentation to help developers understand codebase structure, design patterns, and integration points.',
+    config: {
+      model,
+      temperature: 0.1,
+      prompt,
+    },
+  };
+}

+ 4 - 2
src/agents/index.test.ts

@@ -247,6 +247,7 @@ describe('isSubagent type guard', () => {
     expect(isSubagent('oracle')).toBe(true);
     expect(isSubagent('designer')).toBe(true);
     expect(isSubagent('fixer')).toBe(true);
+    expect(isSubagent('cartography')).toBe(true);
   });
 
   test('returns false for orchestrator', () => {
@@ -290,11 +291,12 @@ describe('createAgents', () => {
     expect(names).toContain('oracle');
     expect(names).toContain('librarian');
     expect(names).toContain('fixer');
+    expect(names).toContain('cartography');
   });
 
-  test('creates exactly 6 agents (1 primary + 5 subagents)', () => {
+  test('creates exactly 7 agents (1 primary + 6 subagents)', () => {
     const agents = createAgents();
-    expect(agents.length).toBe(6);
+    expect(agents.length).toBe(7);
   });
 });
 

+ 2 - 0
src/agents/index.ts

@@ -10,6 +10,7 @@ import {
 } from '../config';
 import { getAgentMcpList } from '../config/agent-mcps';
 
+import { createCartographyAgent } from './cartography';
 import { createDesignerAgent } from './designer';
 import { createExplorerAgent } from './explorer';
 import { createFixerAgent } from './fixer';
@@ -99,6 +100,7 @@ const SUBAGENT_FACTORIES: Record<SubagentName, AgentFactory> = {
   oracle: createOracleAgent,
   designer: createDesignerAgent,
   fixer: createFixerAgent,
+  cartography: createCartographyAgent,
 };
 
 // Public API

+ 8 - 0
src/agents/orchestrator.ts

@@ -51,6 +51,13 @@ You are an AI coding orchestrator that optimizes for quality, speed, cost, and r
 - **Parallelization:** 3+ independent tasks → spawn multiple @fixers. 1-2 simple tasks → do yourself.
 - **Rule of thumb:** Explaining > doing? → yourself. Can split to parallel streams? → multiple @fixers.
 
+@cartography
+- Role: Repository mapping and codemap generation specialist
+- Capabilities: Creates hierarchical documentation of codebase structure, design patterns, and integration points
+- **Delegate when:** User asks to understand/map a repository • Starting work on unfamiliar codebase • Need comprehensive codebase documentation • Want to create/update codemap files for the project
+- **Don't delegate when:** Just need a quick file search or grep • Already familiar with codebase structure • Single specific lookup • Not related to repository understanding
+- **Rule of thumb:** "Help me understand this codebase" → @cartography. "Find this specific file" → @explorer.
+
 </Agents>
 
 <Workflow>
@@ -71,6 +78,7 @@ Each specialist delivers 10x results in their domain:
 - @oracle → High-stakes decisions where wrong choice is costly, not routine calls
 - @designer → User-facing experiences where polish matters, not internal logic
 - @fixer → Parallel execution of clear specs, not explaining trivial changes
+- @cartography → Repository mapping and codemap generation for understanding codebases
 
 **Delegation efficiency:**
 - Reference paths/lines, don't paste files (\`src/app.ts:42\` not full contents)

+ 155 - 1
src/background/background-manager.test.ts

@@ -1319,7 +1319,7 @@ describe('BackgroundTaskManager', () => {
       const ctx = createMockContext();
       const manager = new BackgroundTaskManager(ctx);
 
-      // Orchestrator -> all 5 subagent names
+      // Orchestrator -> all 6 subagent names
       const orchestratorTask = manager.launch({
         agent: 'orchestrator',
         prompt: 'test',
@@ -1340,6 +1340,7 @@ describe('BackgroundTaskManager', () => {
         'oracle',
         'designer',
         'fixer',
+        'cartography',
       ]);
 
       // Fixer -> empty (leaf node)
@@ -1399,7 +1400,160 @@ describe('BackgroundTaskManager', () => {
         'oracle',
         'designer',
         'fixer',
+        'cartography',
       ]);
     });
   });
+
+  describe('listTasks', () => {
+    test('returns empty array when no tasks exist', () => {
+      const ctx = createMockContext();
+      const manager = new BackgroundTaskManager(ctx);
+
+      const tasks = manager.listTasks();
+      expect(tasks).toEqual([]);
+    });
+
+    test('returns all tasks when no filter specified', async () => {
+      const ctx = createMockContext();
+      const manager = new BackgroundTaskManager(ctx);
+
+      manager.launch({
+        agent: 'explorer',
+        prompt: 'test1',
+        description: 'First test task',
+        parentSessionId: 'parent-123',
+      });
+
+      manager.launch({
+        agent: 'fixer',
+        prompt: 'test2',
+        description: 'Second test task',
+        parentSessionId: 'parent-123',
+      });
+
+      const tasks = manager.listTasks();
+      expect(tasks).toHaveLength(2);
+      expect(tasks[0]?.agent).toBe('explorer');
+      expect(tasks[1]?.agent).toBe('fixer');
+    });
+
+    test('filters tasks by status', async () => {
+      const ctx = createMockContext();
+      const manager = new BackgroundTaskManager(ctx);
+
+      const task1 = manager.launch({
+        agent: 'explorer',
+        prompt: 'test1',
+        description: 'First test task',
+        parentSessionId: 'parent-123',
+      });
+
+      // Launch second task but we don't need to reference it
+      manager.launch({
+        agent: 'fixer',
+        prompt: 'test2',
+        description: 'Second test task',
+        parentSessionId: 'parent-123',
+      });
+
+      // Cancel task1
+      manager.cancel(task1.id);
+
+      // Get only cancelled tasks
+      const cancelledTasks = manager.listTasks('cancelled');
+      expect(cancelledTasks).toHaveLength(1);
+      expect(cancelledTasks[0]?.id).toBe(task1.id);
+
+      // Get running tasks (task2 is pending/starting/running)
+      // Note: task2 may be pending depending on timing
+      const runningTasks = manager.listTasks('running');
+      const pendingTasks = manager.listTasks('pending');
+      const activeTasks = runningTasks.length + pendingTasks.length;
+      expect(activeTasks).toBeGreaterThanOrEqual(0);
+    });
+
+    test('returns task with correct fields', () => {
+      const ctx = createMockContext();
+      const manager = new BackgroundTaskManager(ctx);
+
+      const task = manager.launch({
+        agent: 'explorer',
+        prompt: 'test',
+        description: 'Test task description',
+        parentSessionId: 'parent-123',
+      });
+
+      const tasks = manager.listTasks();
+      expect(tasks).toHaveLength(1);
+
+      const listedTask = tasks[0];
+      expect(listedTask).toBeDefined();
+      expect(listedTask?.id).toBe(task.id);
+      expect(listedTask?.agent).toBe('explorer');
+      expect(listedTask?.description).toBe('Test task description');
+      expect(listedTask?.status).toBeDefined();
+      expect(listedTask?.startedAt).toBeInstanceOf(Date);
+      expect(listedTask?.durationMs).toBeGreaterThanOrEqual(0);
+    });
+
+    test('calculates duration for running tasks', async () => {
+      const ctx = createMockContext();
+      const manager = new BackgroundTaskManager(ctx);
+
+      manager.launch({
+        agent: 'explorer',
+        prompt: 'test',
+        description: 'Test task',
+        parentSessionId: 'parent-123',
+      });
+
+      // Wait a bit
+      await new Promise((r) => setTimeout(r, 50));
+
+      const tasks = manager.listTasks();
+      expect(tasks[0]?.durationMs).toBeGreaterThanOrEqual(0);
+    });
+
+    test('includes completedAt for completed tasks', async () => {
+      const ctx = createMockContext({
+        sessionMessagesResult: {
+          data: [
+            {
+              info: { role: 'assistant' },
+              parts: [{ type: 'text', text: 'done' }],
+            },
+          ],
+        },
+      });
+      const manager = new BackgroundTaskManager(ctx);
+
+      const task = manager.launch({
+        agent: 'explorer',
+        prompt: 'test',
+        description: 'Test task',
+        parentSessionId: 'parent-123',
+      });
+
+      // Small delay to ensure non-zero duration
+      await new Promise((r) => setTimeout(r, 10));
+
+      await Promise.resolve();
+      await Promise.resolve();
+
+      // Trigger completion
+      await manager.handleSessionStatus({
+        type: 'session.status',
+        properties: {
+          sessionID: task.sessionId,
+          status: { type: 'idle' },
+        },
+      });
+
+      const tasks = manager.listTasks('completed');
+      expect(tasks).toHaveLength(1);
+      expect(tasks[0]?.completedAt).toBeInstanceOf(Date);
+      expect(tasks[0]?.durationMs).toBeGreaterThanOrEqual(0);
+    });
+  });
 });

+ 37 - 0
src/background/background-manager.ts

@@ -782,6 +782,43 @@ export class BackgroundTaskManager {
   }
 
   /**
+   * List all background tasks with optional status filtering.
+   *
+   * @param statusFilter - Optional status to filter by (e.g., 'running', 'completed')
+   * @returns Array of task summaries
+   */
+  listTasks(statusFilter?: BackgroundTask['status']): Array<{
+    id: string;
+    agent: string;
+    description: string;
+    status: BackgroundTask['status'];
+    startedAt: Date;
+    completedAt?: Date;
+    durationMs: number;
+  }> {
+    const tasks = Array.from(this.tasks.values());
+    const filtered = statusFilter
+      ? tasks.filter((t) => t.status === statusFilter)
+      : tasks;
+
+    return filtered.map((task) => {
+      const durationMs = task.completedAt
+        ? task.completedAt.getTime() - task.startedAt.getTime()
+        : Date.now() - task.startedAt.getTime();
+
+      return {
+        id: task.id,
+        agent: task.agent,
+        description: task.description,
+        status: task.status,
+        startedAt: task.startedAt,
+        completedAt: task.completedAt,
+        durationMs,
+      };
+    });
+  }
+
+  /**
    * Clean up all tasks.
    */
   cleanup(): void {

+ 1 - 1
src/cli/custom-skills.ts

@@ -31,7 +31,7 @@ export const CUSTOM_SKILLS: CustomSkill[] = [
   {
     name: 'cartography',
     description: 'Repository understanding and hierarchical codemap generation',
-    allowedAgents: ['orchestrator', 'explorer'],
+    allowedAgents: ['orchestrator', 'explorer', 'cartography'],
     sourcePath: 'src/skills/cartography',
   },
 ];

+ 12 - 20
src/cli/install.ts

@@ -118,14 +118,12 @@ async function runInstall(config: InstallConfig): Promise<number> {
 
   printHeader(isUpdate);
 
-
   let totalSteps = 4;
   if (config.installSkills) totalSteps += 1;
   if (config.installCustomSkills) totalSteps += 1;
 
   let step = 1;
 
-
   printStep(step++, totalSteps, 'Checking OpenCode installation...');
   if (config.dryRun) {
     printInfo('Dry run mode - skipping OpenCode check');
@@ -133,25 +131,19 @@ async function runInstall(config: InstallConfig): Promise<number> {
     const { ok } = await checkOpenCodeInstalled();
     if (!ok) return 1;
   }
-
-  {
-    printStep(step++, totalSteps, 'Adding oh-my-opencode-slim plugin...');
-    if (config.dryRun) {
-      printInfo('Dry run mode - skipping plugin installation');
-    } else {
-      const pluginResult = await addPluginToOpenCodeConfig();
-      if (!handleStepResult(pluginResult, 'Plugin added')) return 1;
-    }
+  printStep(step++, totalSteps, 'Adding oh-my-opencode-slim plugin...');
+  if (config.dryRun) {
+    printInfo('Dry run mode - skipping plugin installation');
+  } else {
+    const pluginResult = await addPluginToOpenCodeConfig();
+    if (!handleStepResult(pluginResult, 'Plugin added')) return 1;
   }
-
-  {
-    printStep(step++, totalSteps, 'Disabling OpenCode default agents...');
-    if (config.dryRun) {
-      printInfo('Dry run mode - skipping agent disabling');
-    } else {
-      const agentResult = disableDefaultAgents();
-      if (!handleStepResult(agentResult, 'Default agents disabled')) return 1;
-    }
+  printStep(step++, totalSteps, 'Disabling OpenCode default agents...');
+  if (config.dryRun) {
+    printInfo('Dry run mode - skipping agent disabling');
+  } else {
+    const agentResult = disableDefaultAgents();
+    if (!handleStepResult(agentResult, 'Default agents disabled')) return 1;
   }
 
   printStep(step++, totalSteps, 'Writing oh-my-opencode-slim configuration...');

+ 4 - 1
src/cli/providers.ts

@@ -36,7 +36,10 @@ export const MODEL_MAPPINGS = {
     oracle: { model: 'github-copilot/claude-opus-4.6', variant: 'high' },
     librarian: { model: 'github-copilot/grok-code-fast-1', variant: 'low' },
     explorer: { model: 'github-copilot/grok-code-fast-1', variant: 'low' },
-    designer: { model: 'github-copilot/gemini-3.1-pro-preview', variant: 'medium' },
+    designer: {
+      model: 'github-copilot/gemini-3.1-pro-preview',
+      variant: 'medium',
+    },
     fixer: { model: 'github-copilot/claude-sonnet-4.6', variant: 'low' },
   },
   'zai-plan': {

+ 1 - 0
src/config/agent-mcps.ts

@@ -14,6 +14,7 @@ export const DEFAULT_AGENT_MCPS: Record<AgentName, string[]> = {
   librarian: ['websearch', 'context7', 'grep_app'],
   explorer: [],
   fixer: [],
+  cartography: [],
 };
 
 /**

+ 4 - 2
src/config/constants.ts

@@ -10,6 +10,7 @@ export const SUBAGENT_NAMES = [
   'oracle',
   'designer',
   'fixer',
+  'cartography',
 ] as const;
 
 export const ORCHESTRATOR_NAME = 'orchestrator' as const;
@@ -23,7 +24,7 @@ export type AgentName = (typeof ALL_AGENT_NAMES)[number];
 // orchestrator: can spawn all subagents (full delegation)
 // fixer: leaf node — prompt forbids delegation; use grep/glob for lookups
 // designer: can spawn explorer (for research during design)
-// explorer/librarian/oracle: cannot spawn any subagents (leaf nodes)
+// explorer/librarian/oracle/cartography: cannot spawn any subagents (leaf nodes)
 // Unknown agent types not listed here default to explorer-only access
 export const SUBAGENT_DELEGATION_RULES: Record<AgentName, readonly string[]> = {
   orchestrator: SUBAGENT_NAMES,
@@ -32,6 +33,7 @@ export const SUBAGENT_DELEGATION_RULES: Record<AgentName, readonly string[]> = {
   explorer: [],
   librarian: [],
   oracle: [],
+  cartography: [],
 };
 
 // Default models for each agent
@@ -43,6 +45,7 @@ export const DEFAULT_MODELS: Record<AgentName, string | undefined> = {
   explorer: 'openai/gpt-5-codex',
   designer: 'kimi-for-coding/k2p5',
   fixer: 'openai/gpt-5-codex',
+  cartography: 'openai/gpt-5-codex',
 };
 
 // Polling configuration
@@ -57,4 +60,3 @@ export const FALLBACK_FAILOVER_TIMEOUT_MS = 15_000;
 
 // Polling stability
 export const STABLE_POLLS_THRESHOLD = 3;
-

+ 6 - 0
src/config/loader.test.ts

@@ -123,6 +123,12 @@ describe('loadPluginConfig', () => {
             fallback2: 'chutes/kimi-k2.5',
             fallback3: 'opencode/gpt-5-nano',
           },
+          cartography: {
+            primary: 'openai/gpt-5.4',
+            fallback1: 'anthropic/claude-opus-4-6',
+            fallback2: 'chutes/kimi-k2.5',
+            fallback3: 'opencode/gpt-5-nano',
+          },
         },
       }),
     );

+ 4 - 0
src/config/schema.ts

@@ -7,6 +7,7 @@ const FALLBACK_AGENT_NAMES = [
   'explorer',
   'librarian',
   'fixer',
+  'cartography',
 ] as const;
 
 const MANUAL_AGENT_NAMES = [
@@ -16,6 +17,7 @@ const MANUAL_AGENT_NAMES = [
   'explorer',
   'librarian',
   'fixer',
+  'cartography',
 ] as const;
 
 const ProviderModelIdSchema = z
@@ -55,6 +57,7 @@ export const ManualPlanSchema = z
     explorer: ManualAgentPlanSchema,
     librarian: ManualAgentPlanSchema,
     fixer: ManualAgentPlanSchema,
+    cartography: ManualAgentPlanSchema,
   })
   .strict();
 
@@ -72,6 +75,7 @@ const FallbackChainsSchema = z
     explorer: AgentModelChainSchema.optional(),
     librarian: AgentModelChainSchema.optional(),
     fixer: AgentModelChainSchema.optional(),
+    cartography: AgentModelChainSchema.optional(),
   })
   .catchall(AgentModelChainSchema);
 

+ 76 - 1
src/tools/background.ts

@@ -180,5 +180,80 @@ Only cancels pending/starting/running tasks.`,
     },
   });
 
-  return { background_task, background_output, background_cancel };
+  // Tool for listing background tasks
+  const background_list = tool({
+    description: `List all background tasks with optional status filtering.
+
+Returns a table of tasks showing ID, agent, description, status, and duration.
+Use status filter to see only pending, running, completed, failed, or cancelled tasks.`,
+    args: {
+      status: z
+        .enum([
+          'pending',
+          'starting',
+          'running',
+          'completed',
+          'failed',
+          'cancelled',
+        ])
+        .optional()
+        .describe('Filter by status (optional)'),
+    },
+    async execute(args) {
+      const statusFilter = args.status as
+        | 'pending'
+        | 'starting'
+        | 'running'
+        | 'completed'
+        | 'failed'
+        | 'cancelled'
+        | undefined;
+
+      const tasks = manager.listTasks(statusFilter);
+
+      if (tasks.length === 0) {
+        return statusFilter
+          ? `No background tasks with status "${statusFilter}".`
+          : 'No background tasks found.';
+      }
+
+      // Format duration helper
+      const formatDuration = (ms: number): string => {
+        if (ms < 1000) return `${ms}ms`;
+        if (ms < 60000) return `${Math.floor(ms / 1000)}s`;
+        const minutes = Math.floor(ms / 60000);
+        const seconds = Math.floor((ms % 60000) / 1000);
+        return `${minutes}m ${seconds}s`;
+      };
+
+      // Build table output
+      const lines: string[] = [
+        `Found ${tasks.length} background task(s):`,
+        '',
+        'ID | Agent | Description | Status | Duration',
+        '---|-------|-------------|--------|----------',
+      ];
+
+      for (const task of tasks) {
+        const shortId = task.id.slice(0, 12);
+        const desc =
+          task.description.length > 30
+            ? task.description.slice(0, 27) + '...'
+            : task.description;
+        const duration = formatDuration(task.durationMs);
+        lines.push(
+          `${shortId} | ${task.agent} | ${desc} | ${task.status} | ${duration}`,
+        );
+      }
+
+      return lines.join('\n');
+    },
+  });
+
+  return {
+    background_task,
+    background_output,
+    background_cancel,
+    background_list,
+  };
 }