From 8b17fad72365a34a30b563c4488361d6bad3a913 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sun, 28 Dec 2025 19:33:17 +0800 Subject: [PATCH] feat(discovery): enhance discovery progress reading with new schema support --- .claude/commands/issue/plan.md | 61 ++++++++++++++----------- ccw/src/core/routes/discovery-routes.ts | 50 +++++++++++++++++--- 2 files changed, 79 insertions(+), 32 deletions(-) diff --git a/.claude/commands/issue/plan.md b/.claude/commands/issue/plan.md index 5a24f4ab..23c76b54 100644 --- a/.claude/commands/issue/plan.md +++ b/.claude/commands/issue/plan.md @@ -172,19 +172,17 @@ TodoWrite({ }); ``` -### Phase 2: Unified Explore + Plan (issue-plan-agent) +### Phase 2: Unified Explore + Plan (issue-plan-agent) - PARALLEL ```javascript Bash(`mkdir -p .workflow/issues/solutions`); const pendingSelections = []; // Collect multi-solution issues for user selection -for (const [batchIndex, batch] of batches.entries()) { - updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress'); - - // Build issue list with metadata for agent context +// Build prompts for all batches +const agentTasks = batches.map((batch, batchIndex) => { const issueList = batch.map(i => `- ${i.id}: ${i.title}${i.tags.length ? ` [${i.tags.join(', ')}]` : ''}`).join('\n'); + const batchIds = batch.map(i => i.id); - // Build minimal prompt - agent handles exploration, planning, and binding const issuePrompt = ` ## Plan Issues @@ -223,32 +221,43 @@ ${issueList} \`\`\` `; - // Launch issue-plan-agent - agent writes solutions directly - const batchIds = batch.map(i => i.id); - const result = Task( - subagent_type="issue-plan-agent", - run_in_background=false, - description=`Explore & plan ${batch.length} issues: ${batchIds.join(', ')}`, - prompt=issuePrompt - ); + return { batchIndex, batchIds, issuePrompt, batch }; +}); - // Parse summary from agent - const summary = JSON.parse(result); +// Launch agents in parallel (max 10 concurrent) +const MAX_PARALLEL = 10; +for (let i = 0; i < agentTasks.length; i += MAX_PARALLEL) { + const chunk = agentTasks.slice(i, i + MAX_PARALLEL); + const taskIds = []; - // Display auto-bound solutions - for (const item of summary.bound || []) { - console.log(`✓ ${item.issue_id}: ${item.solution_id} (${item.task_count} tasks)`); + // Launch chunk in parallel + for (const { batchIndex, batchIds, issuePrompt, batch } of chunk) { + updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress'); + const taskId = Task( + subagent_type="issue-plan-agent", + run_in_background=true, + description=`Explore & plan ${batch.length} issues: ${batchIds.join(', ')}`, + prompt=issuePrompt + ); + taskIds.push({ taskId, batchIndex }); } - // Collect pending selections for Phase 3 - pendingSelections.push(...(summary.pending_selection || [])); + console.log(`Launched ${taskIds.length} agents (batch ${i/MAX_PARALLEL + 1}/${Math.ceil(agentTasks.length/MAX_PARALLEL)})...`); - // Show conflicts - if (summary.conflicts?.length > 0) { - console.log(`⚠ Conflicts: ${summary.conflicts.map(c => c.file).join(', ')}`); + // Collect results from this chunk + for (const { taskId, batchIndex } of taskIds) { + const result = TaskOutput(task_id=taskId, block=true); + const summary = JSON.parse(result); + + for (const item of summary.bound || []) { + console.log(`✓ ${item.issue_id}: ${item.solution_id} (${item.task_count} tasks)`); + } + pendingSelections.push(...(summary.pending_selection || [])); + if (summary.conflicts?.length > 0) { + console.log(`⚠ Conflicts: ${summary.conflicts.map(c => c.file).join(', ')}`); + } + updateTodo(`Plan batch ${batchIndex + 1}`, 'completed'); } - - updateTodo(`Plan batch ${batchIndex + 1}`, 'completed'); } ``` diff --git a/ccw/src/core/routes/discovery-routes.ts b/ccw/src/core/routes/discovery-routes.ts index 0cc1280b..8ba997f2 100644 --- a/ccw/src/core/routes/discovery-routes.ts +++ b/ccw/src/core/routes/discovery-routes.ts @@ -116,13 +116,51 @@ function readDiscoveryState(discoveriesDir: string, discoveryId: string): any | } function readDiscoveryProgress(discoveriesDir: string, discoveryId: string): any | null { - const progressPath = join(discoveriesDir, discoveryId, 'discovery-progress.json'); - if (!existsSync(progressPath)) return null; - try { - return JSON.parse(readFileSync(progressPath, 'utf8')); - } catch { - return null; + // Try merged state first (new schema) + const statePath = join(discoveriesDir, discoveryId, 'discovery-state.json'); + if (existsSync(statePath)) { + try { + const state = JSON.parse(readFileSync(statePath, 'utf8')); + // New merged schema: perspectives array + results object + if (state.perspectives && Array.isArray(state.perspectives)) { + const completed = state.perspectives.filter((p: any) => p.status === 'completed').length; + const total = state.perspectives.length; + return { + discovery_id: discoveryId, + phase: state.phase, + last_update: state.updated_at || state.created_at, + progress: { + perspective_analysis: { + total, + completed, + in_progress: state.perspectives.filter((p: any) => p.status === 'in_progress').length, + percent_complete: total > 0 ? Math.round((completed / total) * 100) : 0 + }, + external_research: state.external_research || { enabled: false, completed: false }, + aggregation: { completed: state.phase === 'aggregation' || state.phase === 'complete' }, + issue_generation: { completed: state.phase === 'complete', issues_count: state.results?.issues_generated || 0 } + }, + agent_status: state.perspectives + }; + } + // Old schema: metadata.perspectives (backward compat) + if (state.metadata?.perspectives) { + return { + discovery_id: discoveryId, + phase: state.phase, + progress: { perspective_analysis: { total: state.metadata.perspectives.length, completed: state.perspectives_completed?.length || 0 } } + }; + } + } catch { + // Fall through + } } + // Fallback: try legacy progress file + const progressPath = join(discoveriesDir, discoveryId, 'discovery-progress.json'); + if (existsSync(progressPath)) { + try { return JSON.parse(readFileSync(progressPath, 'utf8')); } catch { return null; } + } + return null; } function readPerspectiveFindings(discoveriesDir: string, discoveryId: string): any[] {