diff --git a/CLAUDE.md b/CLAUDE.md index c1d8d669..d2df1146 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,8 +19,8 @@ The extensible trigger system routes events to agents: Trello/GitHub Webhook → TriggerRegistry → Agent → Code Changes → PR ``` -- `src/triggers/` - Event handlers (Trello card moves, labels, GitHub PRs) -- `src/agents/` - AI agents (briefing, planning, implementation, review) +- `src/triggers/` - Event handlers (Trello card moves, labels, GitHub PRs, attachments) +- `src/agents/` - AI agents (briefing, planning, implementation, review, debug) - `src/gadgets/` - Tools agents can use (Trello API, Git operations, file system) ### Multi-Project Support @@ -100,6 +100,8 @@ Optional: ## Debugging Production Sessions +### Manual Session Download + Download session logs and card data from a Trello card for debugging: ```bash @@ -109,3 +111,39 @@ npm run tool:download-session abc123 ``` This downloads all `.gz` log attachments (ungzipped), plus card description, checklists, and comments into a temp directory. + +### Automatic Debug Analysis + +CASCADE includes a debug agent that automatically analyzes agent session logs: + +1. **Automatic Trigger**: When an agent uploads a session log (`.zip` file) to a Trello card, the debug agent automatically triggers +2. **Log Analysis**: The debug agent downloads, extracts, and analyzes the session logs to identify: + - Errors and exceptions + - Failed gadget calls + - Iteration loops and inefficiencies + - Excessive LLM calls + - Scope creep or confusion patterns +3. **Debug Card Creation**: Creates a new card in the DEBUG list with: + - Title: `{agent-type} - {original card name}` + - Executive summary of what went wrong + - Key issues found + - Timeline of events + - Actionable recommendations + - Link back to the original card + +**Setup**: Add a `debug` list to your Trello board and configure it in `config/projects.json`: + +```json +{ + "trello": { + "lists": { + "briefing": "...", + "planning": "...", + "todo": "...", + "debug": "YOUR_DEBUG_LIST_ID" + } + } +} +``` + +The debug agent only analyzes logs uploaded by the authenticated CASCADE user and matching the pattern `{agent-type}-{timestamp}.zip`. diff --git a/config/projects.json b/config/projects.json index 7aec2ab2..063b0d38 100644 --- a/config/projects.json +++ b/config/projects.json @@ -23,7 +23,8 @@ "todo": "694ec3a365a4c75df2493504", "inProgress": "694ec3a5d0dae4cb06d25517", "inReview": "694ec3a8f0359b62fa76ecb9", - "done": "694ec3a935da16c70d20375c" + "done": "694ec3a935da16c70d20375c", + "debug": "TODO_ADD_DEBUG_LIST_ID" }, "labels": { "readyToProcess": "694ec394370da080b52eb6be", diff --git a/package-lock.json b/package-lock.json index 6fd8d6b0..04eb8f9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@biomejs/biome": "^1.9.4", "@commitlint/cli": "^20.1.0", "@commitlint/config-conventional": "^20.0.0", + "@types/adm-zip": "^0.5.7", "@types/node": "^22.10.2", "@vitest/coverage-v8": "^2.1.8", "lefthook": "^1.10.10", @@ -1845,6 +1846,16 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@types/adm-zip": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz", + "integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/archiver": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-7.0.0.tgz", diff --git a/package.json b/package.json index 24204ba4..522a23bb 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@biomejs/biome": "^1.9.4", "@commitlint/cli": "^20.1.0", "@commitlint/config-conventional": "^20.0.0", + "@types/adm-zip": "^0.5.7", "@types/node": "^22.10.2", "@vitest/coverage-v8": "^2.1.8", "lefthook": "^1.10.10", diff --git a/src/agents/base.ts b/src/agents/base.ts index a949e415..bad7595d 100644 --- a/src/agents/base.ts +++ b/src/agents/base.ts @@ -118,6 +118,13 @@ async function buildAgentContext( log: ReturnType, triggerType?: string, prContext?: { prNumber: number; prBranch: string; repoFullName: string; headSha: string }, + debugContext?: { + logDir: string; + originalCardId: string; + originalCardName: string; + originalCardUrl: string; + detectedAgentType: string; + }, ): Promise { // Build prompt context for template rendering const promptContext: PromptContext = { @@ -133,6 +140,14 @@ async function buildAgentContext( headSha: prContext.headSha, triggerType, }), + ...(debugContext && { + logDir: debugContext.logDir, + originalCardId: debugContext.originalCardId, + originalCardName: debugContext.originalCardName, + originalCardUrl: debugContext.originalCardUrl, + detectedAgentType: debugContext.detectedAgentType, + debugListId: project.trello?.lists?.debug, + }), }; // Get system prompt and model @@ -422,9 +437,19 @@ export async function executeAgent( }); try { - // Setup repository (checkout PR branch if provided) - const setup = await setupRepository(project, log, prContext?.prBranch); - repoDir = setup.repoDir; + let installResult: DependencyInstallResult | null = null; + + // Check if this is debug agent with pre-extracted logs + if (input.logDir && typeof input.logDir === 'string') { + // Debug agent: use log directory instead of repo + repoDir = input.logDir; + log.info('Using log directory (no repo setup)', { logDir: input.logDir, agentType }); + } else { + // Normal agents: setup repository (checkout PR branch if provided) + const setup = await setupRepository(project, log, prContext?.prBranch); + repoDir = setup.repoDir; + installResult = setup.installResult; + } log.info('Running agent', { agentType, @@ -434,7 +459,19 @@ export async function executeAgent( repoDir, }); - // Build agent context (with PR context if check-failure flow) + // Extract debug context if this is debug agent + const debugContext = + agentType === 'debug' && input.logDir + ? { + logDir: input.logDir, + originalCardId: input.originalCardId as string, + originalCardName: input.originalCardName as string, + originalCardUrl: input.originalCardUrl as string, + detectedAgentType: input.detectedAgentType as string, + } + : undefined; + + // Build agent context (with PR context if check-failure flow, or debug context if debug agent) const ctx = await buildAgentContext( agentType, cardId, @@ -444,6 +481,7 @@ export async function executeAgent( log, triggerType, prContext, + debugContext, ); // Change to repo directory (llmist gadgets use process.cwd() for path validation) @@ -476,7 +514,7 @@ export async function executeAgent( cardId, ctx.cardData, ctx.contextFiles, - setup.installResult, + installResult, ); // Run the agent diff --git a/src/agents/prompts/index.ts b/src/agents/prompts/index.ts index 33a4243f..96092845 100644 --- a/src/agents/prompts/index.ts +++ b/src/agents/prompts/index.ts @@ -27,6 +27,14 @@ export interface PromptContext { headSha?: string; triggerType?: string; + // Debug-specific + logDir?: string; + originalCardId?: string; + originalCardName?: string; + originalCardUrl?: string; + detectedAgentType?: string; + debugListId?: string; + // Future extensibility [key: string]: unknown; } @@ -47,7 +55,7 @@ function loadTemplate(agentType: string): string { } export function getSystemPrompt(agentType: string, context: PromptContext = {}): string { - const validTypes = ['briefing', 'planning', 'implementation']; + const validTypes = ['briefing', 'planning', 'implementation', 'debug']; if (!validTypes.includes(agentType)) { throw new Error(`Unknown agent type: ${agentType}`); } @@ -60,3 +68,4 @@ export function getSystemPrompt(agentType: string, context: PromptContext = {}): export const BRIEFING_SYSTEM_PROMPT = loadTemplate('briefing'); export const PLANNING_SYSTEM_PROMPT = loadTemplate('planning'); export const IMPLEMENTATION_SYSTEM_PROMPT = loadTemplate('implementation'); +export const DEBUG_SYSTEM_PROMPT = loadTemplate('debug'); diff --git a/src/agents/prompts/templates/debug.eta b/src/agents/prompts/templates/debug.eta new file mode 100644 index 00000000..33da2823 --- /dev/null +++ b/src/agents/prompts/templates/debug.eta @@ -0,0 +1,59 @@ +You are a debugging expert analyzing CASCADE agent session logs to identify issues and provide actionable recommendations. + +## Context + +- **Original Card**: <%= it.originalCardName %> +- **Agent Type**: <%= it.detectedAgentType %> +- **Session Logs Directory**: <%= it.logDir %> +- **Original Card URL**: <%= it.originalCardUrl %> +- **DEBUG List ID**: <%= it.debugListId %> + +## Your Task + +1. **Read and analyze** the session logs from `<%= it.logDir %>`: + - `cascade.log` - Agent execution events and high-level flow + - `llmist.log` - Library-level logging and internal operations + - `llm-calls/` directory - Contains numbered request/response pairs: + - `0001.request`, `0001.response` - First LLM interaction + - `0002.request`, `0002.response` - Second LLM interaction + - And so on... + +2. **Identify issues** such as: + - **Errors and exceptions** - Stack traces, error messages, failed operations + - **Failed gadget calls** - Gadget invocations that returned errors + - **Iteration loops** - Agent repeating the same actions without progress + - **Excessive LLM calls** - Too many iterations or redundant API calls + - **Scope creep** - Agent going beyond the task requirements + - **Confusion patterns** - Agent misunderstanding instructions or context + +3. **Analyze the timeline**: + - Trace the sequence of events chronologically + - Identify when things started going wrong + - Determine the root cause vs symptoms + +4. **Create a debug card** in the DEBUG list with your analysis: + - **Title**: `<%= it.detectedAgentType %> - <%= it.originalCardName %>` + - **Description**: Use markdown formatting with these sections: + - **Executive Summary** - 2-3 sentence overview of what went wrong + - **Key Issues** - Bulleted list of problems found + - **Timeline of Events** - Chronological narrative of what happened + - **Recommendations** - Actionable steps to prevent this in the future + - **Original Card** - Link back using: `[Original Card](<%= it.originalCardUrl %>)` + +## Available Gadgets + +- **ReadFile** - Read log files from `<%= it.logDir %>` +- **listDirectory** - List files in the log directory to discover all logs +- **CreateTrelloCard** - Create the debug card in list ID `<%= it.debugListId %>` +- **PostTrelloComment** - Add comments to cards if needed + +## Guidelines + +- Focus on **actionable insights** that help improve agent prompts or system design +- Be specific about what failed and why +- Recommend concrete improvements (e.g., "Add validation for X", "Update prompt to clarify Y") +- If the agent succeeded but had inefficiencies, note those too +- Keep the analysis concise but thorough +- Use code snippets from logs where relevant to illustrate points + +Start by listing the directory contents to see what logs are available, then read and analyze each log file systematically. diff --git a/src/agents/registry.ts b/src/agents/registry.ts index b8d1f20c..5946d468 100644 --- a/src/agents/registry.ts +++ b/src/agents/registry.ts @@ -36,6 +36,7 @@ export function getRegisteredAgents(): string[] { registerAgent('briefing', executeAgent.bind(null, 'briefing')); registerAgent('planning', executeAgent.bind(null, 'planning')); registerAgent('implementation', executeAgent.bind(null, 'implementation')); +registerAgent('debug', executeAgent.bind(null, 'debug')); registerAgent('review', (input) => executeReviewAgent(input as Parameters[0]), ); diff --git a/src/trello/client.ts b/src/trello/client.ts index ec725b6f..3643d0f4 100644 --- a/src/trello/client.ts +++ b/src/trello/client.ts @@ -238,6 +238,28 @@ export const trelloClient = { })); }, + async getMe(): Promise<{ id: string; fullName: string; username: string }> { + logger.debug('Fetching authenticated member info'); + const apiKey = process.env.TRELLO_API_KEY; + const token = process.env.TRELLO_TOKEN; + const response = await fetch( + `https://api.trello.com/1/members/me?key=${apiKey}&token=${token}`, + ); + if (!response.ok) { + throw new Error(`Failed to fetch member: ${response.status}`); + } + const member = (await response.json()) as { + id?: string; + fullName?: string; + username?: string; + }; + return { + id: member.id || '', + fullName: member.fullName || '', + username: member.username || '', + }; + }, + async getListCards(listId: string): Promise { logger.debug('Fetching cards from list', { listId }); const cards = await getClient().lists.getListCards({ id: listId }); diff --git a/src/triggers/index.ts b/src/triggers/index.ts index 7a25dc46..8fb4d313 100644 --- a/src/triggers/index.ts +++ b/src/triggers/index.ts @@ -2,6 +2,7 @@ import { CheckSuiteFailureTrigger } from './github/check-suite-failure.js'; import { PRReadyToMergeTrigger } from './github/pr-ready-to-merge.js'; import { PRReviewCommentTrigger } from './github/pr-review-comment.js'; import type { TriggerRegistry } from './registry.js'; +import { AttachmentAddedTrigger } from './trello/attachment-added.js'; import { CardMovedToBriefingTrigger, CardMovedToPlanningTrigger, @@ -29,6 +30,9 @@ export function registerBuiltInTriggers(registry: TriggerRegistry): void { // Trello: Label triggers registry.register(new ReadyToProcessLabelTrigger()); + // Trello: Attachment triggers + registry.register(new AttachmentAddedTrigger()); + // GitHub: PR review comment trigger registry.register(new PRReviewCommentTrigger()); diff --git a/src/triggers/trello/attachment-added.ts b/src/triggers/trello/attachment-added.ts new file mode 100644 index 00000000..ee0c9c73 --- /dev/null +++ b/src/triggers/trello/attachment-added.ts @@ -0,0 +1,174 @@ +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import AdmZip from 'adm-zip'; +import { trelloClient } from '../../trello/client.js'; +import type { TriggerContext, TriggerHandler, TriggerResult } from '../../types/index.js'; +import { logger } from '../../utils/logging.js'; +import type { TrelloWebhookPayload } from '../types.js'; +import { isTrelloWebhookPayload } from '../types.js'; + +// Cache authenticated user ID to avoid repeated API calls +let cachedMemberId: string | null = null; + +async function getAuthenticatedMemberId(): Promise { + if (cachedMemberId) { + return cachedMemberId; + } + const me = await trelloClient.getMe(); + cachedMemberId = me.id; + logger.info('Cached authenticated member ID', { memberId: cachedMemberId }); + return cachedMemberId; +} + +/** + * Pattern for agent session log files: {agent-type}-{timestamp}.zip + * Examples: + * - implementation-2026-01-02T12-34-56-789Z.zip + * - briefing-timeout-2026-01-02T12-34-56-789Z.zip + */ +function parseAgentLogFilename(filename: string): { agentType: string } | null { + // Match pattern: {agent-type}-{timestamp}.zip + // Allow optional "timeout-" after agent type + const match = filename.match(/^([a-z]+)(?:-timeout)?-[\d-TZ]+\.zip$/i); + if (!match) { + return null; + } + return { + agentType: match[1].toLowerCase(), + }; +} + +async function downloadAndExtractZip(url: string, destDir: string): Promise { + logger.debug('Downloading zip attachment', { url, destDir }); + + // Download with Trello OAuth headers + const apiKey = process.env.TRELLO_API_KEY; + const token = process.env.TRELLO_TOKEN; + const response = await fetch(url, { + headers: { + Authorization: `OAuth oauth_consumer_key="${apiKey}", oauth_token="${token}"`, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to download attachment: ${response.status}`); + } + + // Download to buffer + const buffer = Buffer.from(await response.arrayBuffer()); + + // Extract ZIP + const zip = new AdmZip(buffer); + zip.extractAllTo(destDir, true); + + logger.info('Extracted zip attachment', { destDir, fileCount: zip.getEntries().length }); +} + +export class AttachmentAddedTrigger implements TriggerHandler { + name = 'attachment-added-to-card'; + description = 'Triggers debug agent when agent session log zip is uploaded'; + + matches(ctx: TriggerContext): boolean { + if (ctx.source !== 'trello') return false; + if (!isTrelloWebhookPayload(ctx.payload)) return false; + + const payload = ctx.payload; + + // Check if it's an attachment action + if (payload.action.type !== 'addAttachmentToCard') { + return false; + } + + // Check if attachment exists and is a .zip file + const attachment = payload.action.data.attachment; + if (!attachment || !attachment.name.endsWith('.zip')) { + return false; + } + + // Check if filename matches agent log pattern + const parsed = parseAgentLogFilename(attachment.name); + if (!parsed) { + logger.debug('Attachment does not match agent log pattern', { + filename: attachment.name, + }); + return false; + } + + // Check if DEBUG list is configured + const debugListId = ctx.project.trello.lists.debug; + if (!debugListId) { + logger.warn('DEBUG list not configured, skipping debug agent trigger', { + projectId: ctx.project.id, + }); + return false; + } + + return true; + } + + async handle(ctx: TriggerContext): Promise { + const payload = ctx.payload as TrelloWebhookPayload; + const cardId = payload.action.data.card?.id; + const cardName = payload.action.data.card?.name; + const attachment = payload.action.data.attachment; + + if (!cardId || !cardName || !attachment) { + throw new Error('Missing card or attachment data in payload'); + } + + // Verify uploader is the authenticated user + const authenticatedMemberId = await getAuthenticatedMemberId(); + if (payload.action.idMemberCreator !== authenticatedMemberId) { + logger.info('Attachment uploaded by different user, skipping', { + uploaderId: payload.action.idMemberCreator, + authenticatedId: authenticatedMemberId, + }); + return null; + } + + // Parse agent type from filename + const parsed = parseAgentLogFilename(attachment.name); + if (!parsed) { + // This shouldn't happen since matches() already checked, but be safe + return null; + } + + logger.info('Processing agent log attachment', { + cardId, + cardName, + filename: attachment.name, + agentType: parsed.agentType, + }); + + // Create temp directory for extracted logs + const timestamp = Date.now(); + const logDir = join(tmpdir(), `debug-${cardId}-${timestamp}`); + + try { + // Download and extract the zip + await downloadAndExtractZip(attachment.url, logDir); + + // Get original card URL + const card = await trelloClient.getCard(cardId); + + return { + agentType: 'debug', + agentInput: { + logDir, + originalCardId: cardId, + originalCardName: cardName, + originalCardUrl: card.shortUrl, + detectedAgentType: parsed.agentType, + }, + cardId, // For potential log attachment back to original card + }; + } catch (err) { + logger.error('Failed to download/extract attachment', { + error: String(err), + cardId, + attachmentName: attachment.name, + }); + throw err; + } + } +} diff --git a/src/triggers/trello/webhook-handler.ts b/src/triggers/trello/webhook-handler.ts index d19917ed..19064a47 100644 --- a/src/triggers/trello/webhook-handler.ts +++ b/src/triggers/trello/webhook-handler.ts @@ -239,6 +239,19 @@ export async function processTrelloWebhook( await safeAddComment(result.cardId, `❌ Error: ${String(err)}`); } } finally { + // Cleanup temp directory if created by attachment trigger + if (result?.agentInput?.logDir && typeof result.agentInput.logDir === 'string') { + const logDir = result.agentInput.logDir as string; + if (logDir.startsWith('/tmp/debug-')) { + logger.info('Cleaning up debug temp directory', { logDir }); + const { cleanupTempDir } = await import('../../utils/repo.js'); + await safeOperation(async () => await cleanupTempDir(logDir), { + action: 'cleanup temp directory', + logDir, + }); + } + } + setProcessing(false); processNextQueuedWebhook(config, registry); } diff --git a/src/triggers/types.ts b/src/triggers/types.ts index cc805759..90d76e6a 100644 --- a/src/triggers/types.ts +++ b/src/triggers/types.ts @@ -41,6 +41,13 @@ export interface TrelloWebhookPayload { name: string; shortLink: string; }; + attachment?: { + id: string; + name: string; + url: string; + mimeType: string; + bytes?: number; + }; old?: Record; }; memberCreator?: { diff --git a/src/types/index.ts b/src/types/index.ts index a575494d..bd5d4448 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -15,6 +15,13 @@ export interface AgentInput { headSha?: string; triggerType?: 'check-failure' | 'feature-implementation'; + // Debug agent fields + logDir?: string; + originalCardId?: string; + originalCardName?: string; + originalCardUrl?: string; + detectedAgentType?: string; + [key: string]: unknown; }