mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat: enhanced multi-agent plan generation for Ralph wizard
- Add plan-orchestrator.ts with parallel subagent spawning - Phase 1: 4 specialist subagents analyze in parallel (requirements, architecture, testing, risks) - Phase 2: Synthesis merges and deduplicates outputs - Phase 3: Verification subagent assigns priorities and identifies gaps - New /api/generate-plan-detailed endpoint with SSE progress updates - Auto-regenerate plan when switching between Standard/Enhanced modes - Show quality score, warnings, and gaps from verification - Rename "Detailed" to "Enhanced" button with tooltip Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,7 @@ Claudeman is a Claude Code session manager with a web interface and autonomous R
|
||||
|
||||
When adding new features, always ask: "Will this maintain responsiveness with 20 sessions and 50 agent windows?"
|
||||
|
||||
**Version**: 0.1399 (must match `package.json`)
|
||||
**Version**: 0.1400 (must match `package.json`)
|
||||
|
||||
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, Server-Sent Events, node-pty
|
||||
|
||||
@@ -359,6 +359,7 @@ claudeman reset # Reset all state
|
||||
| `src/ai-plan-checker.ts` | Spawns Claude to detect plan mode prompts for auto-accept |
|
||||
| `src/ralph-tracker.ts` | Detects `<promise>PHRASE</promise>`, todos, loop status |
|
||||
| `src/ralph-config.ts` | Parses `.claude/ralph-loop.local.md` and CLAUDE.md for Ralph config |
|
||||
| `src/plan-orchestrator.ts` | Multi-agent plan generation with parallel analysis + verification |
|
||||
| `src/run-summary.ts` | Tracks session events for "what happened while away" summaries |
|
||||
|
||||
**Spawn Protocol (Autonomous Agents):**
|
||||
@@ -854,6 +855,24 @@ The `RalphTracker` class (`src/ralph-tracker.ts`) detects Ralph patterns in Clau
|
||||
3. Optionally generates a task plan (`@fix_plan.md`)
|
||||
4. Sends the initial prompt with iteration protocol
|
||||
|
||||
**Plan Generation Modes** (Step 2 of wizard):
|
||||
| Mode | Description | API Endpoint |
|
||||
|------|-------------|--------------|
|
||||
| **Brief** | High-level milestones only | `/api/generate-plan` |
|
||||
| **Standard** | Balanced implementation steps | `/api/generate-plan` |
|
||||
| **Enhanced** | Multi-agent orchestration with verification | `/api/generate-plan-detailed` |
|
||||
|
||||
**Enhanced Plan Generation** (`src/plan-orchestrator.ts`): When "Enhanced" mode is selected, the plan is generated using parallel subagent orchestration:
|
||||
1. **Phase 1 - Parallel Analysis**: Spawns 4 specialist subagents simultaneously:
|
||||
- Requirements Analyst → Extracts explicit/implicit requirements
|
||||
- Architecture Planner → Identifies modules, interfaces, types
|
||||
- TDD Specialist → Designs test-first approach, edge cases
|
||||
- Risk Analyst → Identifies failure points, dependencies, blockers
|
||||
2. **Phase 2 - Synthesis**: Merges outputs, deduplicates, orders by dependency
|
||||
3. **Phase 3 - Verification**: Review subagent validates plan, assigns P0/P1/P2 priorities, identifies gaps
|
||||
|
||||
The enhanced mode takes longer (~60-90s) but produces more thorough plans with quality scores. Switching between modes auto-regenerates the plan.
|
||||
|
||||
**Respawn for Ralph Loops**: Disabled by default (checkbox unchecked). When enabled, the respawn controller uses Ralph-specific prompts:
|
||||
- **Update Prompt**: Instructs Claude to document progress to CLAUDE.md, update planning files (`@fix_plan.md`), mark completed tasks, and write a summary before `/clear`
|
||||
- **Kickstart Prompt**: After `/init`, tells Claude it's in a Ralph Wiggum loop and to continue work by reading `@fix_plan.md` and CLAUDE.md notes, then resume on uncompleted tasks
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "0.1399",
|
||||
"version": "0.1400",
|
||||
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -0,0 +1,636 @@
|
||||
/**
|
||||
* @fileoverview Enhanced plan generation using subagent orchestration.
|
||||
*
|
||||
* This module implements a multi-phase plan generation system that leverages
|
||||
* Claude's subagent capabilities for parallel analysis and verification.
|
||||
*
|
||||
* Architecture:
|
||||
* 1. Phase 1 (Parallel Analysis): Spawn 4 specialist subagents simultaneously
|
||||
* 2. Phase 2 (Synthesis): Merge and deduplicate outputs
|
||||
* 3. Phase 3 (Verification): Final review and priority assignment
|
||||
*
|
||||
* @see https://code.claude.com/docs/en/sub-agents
|
||||
* @module plan-orchestrator
|
||||
*/
|
||||
|
||||
import { Session } from './session.js';
|
||||
import { ScreenManager } from './screen-manager.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface PlanItem {
|
||||
content: string;
|
||||
priority: 'P0' | 'P1' | 'P2' | null;
|
||||
source?: string;
|
||||
rationale?: string;
|
||||
phase?: number;
|
||||
}
|
||||
|
||||
export interface SubagentResult {
|
||||
agentType: 'requirements' | 'architecture' | 'testing' | 'risks';
|
||||
items: Array<{
|
||||
category: string;
|
||||
content: string;
|
||||
rationale?: string;
|
||||
}>;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
export interface SynthesisResult {
|
||||
items: PlanItem[];
|
||||
stats: {
|
||||
totalFromSubagents: number;
|
||||
afterDedup: number;
|
||||
sourceBreakdown: Record<string, number>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface VerificationResult {
|
||||
validatedPlan: PlanItem[];
|
||||
gaps: string[];
|
||||
warnings: string[];
|
||||
qualityScore: number;
|
||||
}
|
||||
|
||||
export interface DetailedPlanResult {
|
||||
success: boolean;
|
||||
items?: PlanItem[];
|
||||
costUsd?: number;
|
||||
metadata?: {
|
||||
subagentResults: SubagentResult[];
|
||||
synthesisStats: SynthesisResult['stats'];
|
||||
verificationGaps: string[];
|
||||
verificationWarnings: string[];
|
||||
qualityScore: number;
|
||||
totalDurationMs: number;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type ProgressCallback = (phase: string, detail: string) => void;
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
const SUBAGENT_TIMEOUT_MS = 45000; // 45 seconds per subagent
|
||||
const VERIFICATION_TIMEOUT_MS = 60000; // 60 seconds for verification
|
||||
const MODEL_ANALYSIS = 'haiku'; // Fast model for parallel analysis
|
||||
const MODEL_VERIFICATION = 'sonnet'; // Better reasoning for verification
|
||||
|
||||
// ============================================================================
|
||||
// Subagent Prompts
|
||||
// ============================================================================
|
||||
|
||||
const REQUIREMENTS_ANALYST_PROMPT = `You are a Requirements Analyst specializing in extracting all requirements from task descriptions.
|
||||
|
||||
## YOUR TASK
|
||||
Analyze the following task and extract ALL requirements (explicit and implicit):
|
||||
|
||||
## TASK DESCRIPTION
|
||||
{TASK}
|
||||
|
||||
## INSTRUCTIONS
|
||||
1. Identify explicit requirements (directly stated)
|
||||
2. Infer implicit requirements (unstated but necessary)
|
||||
3. Note any assumptions that should be validated
|
||||
4. Consider non-functional requirements (performance, security, usability)
|
||||
|
||||
## OUTPUT FORMAT
|
||||
Return ONLY a JSON array:
|
||||
[
|
||||
{"category": "functional|non-functional|constraint|assumption", "content": "requirement description", "rationale": "why this is needed"}
|
||||
]
|
||||
|
||||
Generate 8-15 items. Be thorough - missing requirements cause project failures.`;
|
||||
|
||||
const ARCHITECTURE_PLANNER_PROMPT = `You are an Architecture Planner specializing in software component design.
|
||||
|
||||
## YOUR TASK
|
||||
Design the architecture for implementing this task:
|
||||
|
||||
## TASK DESCRIPTION
|
||||
{TASK}
|
||||
|
||||
## INSTRUCTIONS
|
||||
1. Identify all modules/components needed
|
||||
2. Define interfaces between components
|
||||
3. Specify data structures and types
|
||||
4. Note configuration and setup requirements
|
||||
5. Consider separation of concerns
|
||||
|
||||
## OUTPUT FORMAT
|
||||
Return ONLY a JSON array:
|
||||
[
|
||||
{"category": "module|interface|type|config|infrastructure", "content": "component description", "rationale": "why needed"}
|
||||
]
|
||||
|
||||
Generate 10-20 items. Think about the complete system architecture.`;
|
||||
|
||||
const TESTING_SPECIALIST_PROMPT = `You are a TDD Specialist designing a comprehensive test strategy.
|
||||
|
||||
## YOUR TASK
|
||||
Design test coverage for this task:
|
||||
|
||||
## TASK DESCRIPTION
|
||||
{TASK}
|
||||
|
||||
## INSTRUCTIONS
|
||||
Following Test-Driven Development methodology:
|
||||
1. Design unit tests for each component
|
||||
2. Plan integration tests for feature interactions
|
||||
3. Identify edge cases and boundary conditions
|
||||
4. Consider error scenarios and failure modes
|
||||
5. Plan verification steps
|
||||
|
||||
## OUTPUT FORMAT
|
||||
Return ONLY a JSON array:
|
||||
[
|
||||
{"category": "unit|integration|edge-case|error|verification", "content": "test description", "rationale": "what it validates"}
|
||||
]
|
||||
|
||||
Generate 12-25 items. Tests should be written BEFORE implementation.`;
|
||||
|
||||
const RISK_ANALYST_PROMPT = `You are a Risk Analyst identifying potential issues and blockers.
|
||||
|
||||
## YOUR TASK
|
||||
Identify risks and edge cases for this task:
|
||||
|
||||
## TASK DESCRIPTION
|
||||
{TASK}
|
||||
|
||||
## INSTRUCTIONS
|
||||
1. Identify potential failure points
|
||||
2. Note edge cases that could cause bugs
|
||||
3. Consider security vulnerabilities
|
||||
4. Flag performance concerns
|
||||
5. Identify dependencies that could block progress
|
||||
|
||||
## OUTPUT FORMAT
|
||||
Return ONLY a JSON array:
|
||||
[
|
||||
{"category": "failure|edge-case|security|performance|dependency", "content": "risk description", "rationale": "mitigation approach"}
|
||||
]
|
||||
|
||||
Generate 8-15 items. Being proactive about risks prevents surprises.`;
|
||||
|
||||
const VERIFICATION_PROMPT = `You are a Plan Verification Expert reviewing an implementation plan for completeness and quality.
|
||||
|
||||
## ORIGINAL TASK
|
||||
{TASK}
|
||||
|
||||
## SYNTHESIZED PLAN (from multiple analysis subagents)
|
||||
{PLAN}
|
||||
|
||||
## YOUR MISSION
|
||||
Review this plan and:
|
||||
1. Assign priorities (P0=critical/blocking, P1=required, P2=enhancement)
|
||||
2. Identify any gaps or missing steps
|
||||
3. Check logical ordering (tests before implementation, setup before coding)
|
||||
4. Flag potential issues or warnings
|
||||
5. Calculate an overall quality score (0.0-1.0)
|
||||
|
||||
## PRIORITY GUIDELINES
|
||||
- P0: Foundation tasks, type definitions, project setup, blocking dependencies
|
||||
- P1: Core implementation, tests, main features, error handling
|
||||
- P2: Polish, optimization, documentation, nice-to-have features
|
||||
|
||||
## OUTPUT FORMAT
|
||||
Return ONLY a JSON object:
|
||||
{
|
||||
"validatedPlan": [
|
||||
{"content": "step description", "priority": "P0|P1|P2", "rationale": "why this priority"}
|
||||
],
|
||||
"gaps": ["missing requirement 1", "missing test coverage for X"],
|
||||
"warnings": ["consider Y before Z", "potential issue with..."],
|
||||
"qualityScore": 0.85
|
||||
}
|
||||
|
||||
Be critical but constructive. A thorough review catches issues early.`;
|
||||
|
||||
// ============================================================================
|
||||
// Main Orchestrator Class
|
||||
// ============================================================================
|
||||
|
||||
export class PlanOrchestrator {
|
||||
private screenManager: ScreenManager;
|
||||
private workingDir: string;
|
||||
|
||||
constructor(screenManager: ScreenManager, workingDir: string = process.cwd()) {
|
||||
this.screenManager = screenManager;
|
||||
this.workingDir = workingDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a detailed implementation plan using subagent orchestration.
|
||||
*
|
||||
* Phases:
|
||||
* 1. Spawn 4 specialist subagents in parallel for analysis
|
||||
* 2. Synthesize their outputs into a unified plan
|
||||
* 3. Run verification subagent for quality assurance
|
||||
*/
|
||||
async generateDetailedPlan(
|
||||
taskDescription: string,
|
||||
onProgress?: ProgressCallback
|
||||
): Promise<DetailedPlanResult> {
|
||||
const startTime = Date.now();
|
||||
let totalCost = 0;
|
||||
|
||||
try {
|
||||
// Phase 1: Parallel Analysis
|
||||
onProgress?.('parallel-analysis', 'Spawning analysis subagents...');
|
||||
const subagentResults = await this.runParallelAnalysis(taskDescription, onProgress);
|
||||
|
||||
totalCost += subagentResults.reduce((sum, r) => sum + (r.success ? 0.002 : 0), 0); // Estimate
|
||||
|
||||
// Check if we got enough results to continue
|
||||
const successfulResults = subagentResults.filter(r => r.success);
|
||||
if (successfulResults.length < 2) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Only ${successfulResults.length} subagents succeeded. Falling back to standard generation.`,
|
||||
};
|
||||
}
|
||||
|
||||
// Phase 2: Synthesis
|
||||
onProgress?.('synthesis', 'Synthesizing subagent outputs...');
|
||||
const synthesisResult = this.synthesizeResults(subagentResults);
|
||||
|
||||
// Phase 3: Verification
|
||||
onProgress?.('verification', 'Running verification subagent...');
|
||||
const verificationResult = await this.runVerification(
|
||||
taskDescription,
|
||||
synthesisResult.items,
|
||||
onProgress
|
||||
);
|
||||
|
||||
totalCost += 0.01; // Verification cost estimate
|
||||
|
||||
const totalDurationMs = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
items: verificationResult.validatedPlan,
|
||||
costUsd: totalCost,
|
||||
metadata: {
|
||||
subagentResults,
|
||||
synthesisStats: synthesisResult.stats,
|
||||
verificationGaps: verificationResult.gaps,
|
||||
verificationWarnings: verificationResult.warnings,
|
||||
qualityScore: verificationResult.qualityScore,
|
||||
totalDurationMs,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all 4 analysis subagents in parallel.
|
||||
*/
|
||||
private async runParallelAnalysis(
|
||||
taskDescription: string,
|
||||
onProgress?: ProgressCallback
|
||||
): Promise<SubagentResult[]> {
|
||||
const subagents: Array<{
|
||||
type: SubagentResult['agentType'];
|
||||
prompt: string;
|
||||
}> = [
|
||||
{ type: 'requirements', prompt: REQUIREMENTS_ANALYST_PROMPT.replace('{TASK}', taskDescription) },
|
||||
{ type: 'architecture', prompt: ARCHITECTURE_PLANNER_PROMPT.replace('{TASK}', taskDescription) },
|
||||
{ type: 'testing', prompt: TESTING_SPECIALIST_PROMPT.replace('{TASK}', taskDescription) },
|
||||
{ type: 'risks', prompt: RISK_ANALYST_PROMPT.replace('{TASK}', taskDescription) },
|
||||
];
|
||||
|
||||
// Run all subagents in parallel
|
||||
const promises = subagents.map(({ type, prompt }) =>
|
||||
this.runSubagent(type, prompt, onProgress)
|
||||
);
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single analysis subagent.
|
||||
*/
|
||||
private async runSubagent(
|
||||
agentType: SubagentResult['agentType'],
|
||||
prompt: string,
|
||||
onProgress?: ProgressCallback
|
||||
): Promise<SubagentResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
const session = new Session({
|
||||
workingDir: this.workingDir,
|
||||
screenManager: this.screenManager,
|
||||
useScreen: false,
|
||||
mode: 'claude',
|
||||
});
|
||||
|
||||
try {
|
||||
onProgress?.('subagent', `Running ${agentType} analysis...`);
|
||||
|
||||
const { result } = await Promise.race([
|
||||
session.runPrompt(prompt, { model: MODEL_ANALYSIS }),
|
||||
this.timeout(SUBAGENT_TIMEOUT_MS),
|
||||
]);
|
||||
|
||||
// Parse JSON from result
|
||||
const jsonMatch = result.match(/\[[\s\S]*\]/);
|
||||
if (!jsonMatch) {
|
||||
return {
|
||||
agentType,
|
||||
items: [],
|
||||
success: false,
|
||||
error: 'No JSON array found in response',
|
||||
durationMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
if (!Array.isArray(parsed)) {
|
||||
return {
|
||||
agentType,
|
||||
items: [],
|
||||
success: false,
|
||||
error: 'Response is not an array',
|
||||
durationMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
|
||||
const items = parsed.map((item: unknown) => {
|
||||
if (typeof item !== 'object' || item === null) {
|
||||
return { category: 'unknown', content: String(item) };
|
||||
}
|
||||
const obj = item as Record<string, unknown>;
|
||||
return {
|
||||
category: String(obj.category || 'general'),
|
||||
content: String(obj.content || ''),
|
||||
rationale: obj.rationale ? String(obj.rationale) : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
onProgress?.('subagent', `${agentType} complete (${items.length} items)`);
|
||||
|
||||
return {
|
||||
agentType,
|
||||
items,
|
||||
success: true,
|
||||
durationMs: Date.now() - startTime,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
agentType,
|
||||
items: [],
|
||||
success: false,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
durationMs: Date.now() - startTime,
|
||||
};
|
||||
} finally {
|
||||
try {
|
||||
await session.stop();
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesize results from all subagents into a unified plan.
|
||||
*/
|
||||
private synthesizeResults(subagentResults: SubagentResult[]): SynthesisResult {
|
||||
const allItems: PlanItem[] = [];
|
||||
const sourceBreakdown: Record<string, number> = {};
|
||||
|
||||
// Collect items from all successful subagents
|
||||
for (const result of subagentResults) {
|
||||
if (!result.success) continue;
|
||||
|
||||
sourceBreakdown[result.agentType] = result.items.length;
|
||||
|
||||
for (const item of result.items) {
|
||||
allItems.push({
|
||||
content: item.content,
|
||||
priority: null, // Will be assigned by verification
|
||||
source: result.agentType,
|
||||
rationale: item.rationale,
|
||||
phase: this.determinePhase(item.content, result.agentType),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const totalFromSubagents = allItems.length;
|
||||
|
||||
// Deduplicate similar items
|
||||
const deduped = this.deduplicateItems(allItems);
|
||||
|
||||
// Sort by phase
|
||||
deduped.sort((a, b) => (a.phase || 4) - (b.phase || 4));
|
||||
|
||||
return {
|
||||
items: deduped,
|
||||
stats: {
|
||||
totalFromSubagents,
|
||||
afterDedup: deduped.length,
|
||||
sourceBreakdown,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the implementation phase for an item based on keywords.
|
||||
*/
|
||||
private determinePhase(content: string, source: string): number {
|
||||
const lower = content.toLowerCase();
|
||||
|
||||
// Phase 1: Foundation
|
||||
if (
|
||||
lower.includes('create') && (lower.includes('project') || lower.includes('directory')) ||
|
||||
lower.includes('setup') ||
|
||||
lower.includes('initialize') ||
|
||||
lower.includes('configure') ||
|
||||
lower.includes('install') ||
|
||||
lower.includes('define') && (lower.includes('interface') || lower.includes('type'))
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Phase 2: Tests (before implementation)
|
||||
if (
|
||||
source === 'testing' ||
|
||||
lower.includes('test') ||
|
||||
lower.includes('verify') && !lower.includes('final')
|
||||
) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Phase 3: Implementation
|
||||
if (
|
||||
lower.includes('implement') ||
|
||||
lower.includes('build') ||
|
||||
lower.includes('add') ||
|
||||
lower.includes('create') && !lower.includes('test')
|
||||
) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
// Phase 4: Integration & Verification
|
||||
if (
|
||||
lower.includes('integrate') ||
|
||||
lower.includes('connect') ||
|
||||
lower.includes('final') ||
|
||||
lower.includes('run') && lower.includes('suite')
|
||||
) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
return 3; // Default to implementation phase
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduplicate similar items using fuzzy matching.
|
||||
*/
|
||||
private deduplicateItems(items: PlanItem[]): PlanItem[] {
|
||||
const result: PlanItem[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const isDuplicate = result.some(existing =>
|
||||
this.isSimilar(existing.content, item.content)
|
||||
);
|
||||
|
||||
if (!isDuplicate) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two strings are similar (>60% word overlap).
|
||||
*/
|
||||
private isSimilar(a: string, b: string): boolean {
|
||||
const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(w => w.length > 3));
|
||||
const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(w => w.length > 3));
|
||||
|
||||
if (wordsA.size === 0 || wordsB.size === 0) return false;
|
||||
|
||||
let overlap = 0;
|
||||
for (const word of wordsA) {
|
||||
if (wordsB.has(word)) overlap++;
|
||||
}
|
||||
|
||||
const similarity = overlap / Math.min(wordsA.size, wordsB.size);
|
||||
return similarity > 0.6;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the verification subagent to validate and prioritize the plan.
|
||||
*/
|
||||
private async runVerification(
|
||||
taskDescription: string,
|
||||
synthesizedItems: PlanItem[],
|
||||
onProgress?: ProgressCallback
|
||||
): Promise<VerificationResult> {
|
||||
const session = new Session({
|
||||
workingDir: this.workingDir,
|
||||
screenManager: this.screenManager,
|
||||
useScreen: false,
|
||||
mode: 'claude',
|
||||
});
|
||||
|
||||
try {
|
||||
// Format plan for verification
|
||||
const planText = synthesizedItems
|
||||
.map((item, idx) => `${idx + 1}. [Phase ${item.phase}] ${item.content}`)
|
||||
.join('\n');
|
||||
|
||||
const prompt = VERIFICATION_PROMPT
|
||||
.replace('{TASK}', taskDescription)
|
||||
.replace('{PLAN}', planText);
|
||||
|
||||
onProgress?.('verification', 'Validating plan quality...');
|
||||
|
||||
const { result } = await Promise.race([
|
||||
session.runPrompt(prompt, { model: MODEL_VERIFICATION }),
|
||||
this.timeout(VERIFICATION_TIMEOUT_MS),
|
||||
]);
|
||||
|
||||
// Parse JSON from result
|
||||
const jsonMatch = result.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
// Fallback: return items with default priorities
|
||||
return this.fallbackVerification(synthesizedItems);
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
|
||||
const validatedPlan: PlanItem[] = (parsed.validatedPlan || []).map((item: unknown) => {
|
||||
if (typeof item !== 'object' || item === null) {
|
||||
return { content: String(item), priority: 'P1' as const };
|
||||
}
|
||||
const obj = item as Record<string, unknown>;
|
||||
let priority: PlanItem['priority'] = null;
|
||||
if (obj.priority === 'P0' || obj.priority === 'P1' || obj.priority === 'P2') {
|
||||
priority = obj.priority;
|
||||
}
|
||||
return {
|
||||
content: String(obj.content || ''),
|
||||
priority,
|
||||
rationale: obj.rationale ? String(obj.rationale) : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
onProgress?.('verification', `Verification complete (quality: ${Math.round((parsed.qualityScore || 0.8) * 100)}%)`);
|
||||
|
||||
return {
|
||||
validatedPlan,
|
||||
gaps: Array.isArray(parsed.gaps) ? parsed.gaps.map(String) : [],
|
||||
warnings: Array.isArray(parsed.warnings) ? parsed.warnings.map(String) : [],
|
||||
qualityScore: typeof parsed.qualityScore === 'number' ? parsed.qualityScore : 0.8,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('[PlanOrchestrator] Verification failed:', err);
|
||||
return this.fallbackVerification(synthesizedItems);
|
||||
} finally {
|
||||
try {
|
||||
await session.stop();
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback verification when the verification subagent fails.
|
||||
*/
|
||||
private fallbackVerification(items: PlanItem[]): VerificationResult {
|
||||
return {
|
||||
validatedPlan: items.map(item => ({
|
||||
...item,
|
||||
priority: item.phase === 1 ? 'P0' as const :
|
||||
item.phase === 4 ? 'P2' as const : 'P1' as const,
|
||||
})),
|
||||
gaps: [],
|
||||
warnings: ['Verification subagent failed - using heuristic priorities'],
|
||||
qualityScore: 0.7,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a timeout promise.
|
||||
*/
|
||||
private timeout(ms: number): Promise<never> {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
|
||||
});
|
||||
}
|
||||
}
|
||||
+170
-15
@@ -1880,6 +1880,34 @@ class ClaudemanApp {
|
||||
}
|
||||
}, 5 * 60 * 1000); // 5 minutes
|
||||
});
|
||||
|
||||
// Plan generation progress events (for detailed mode with subagents)
|
||||
this.eventSource.addEventListener('plan:progress', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
console.log('[Plan Progress]', data);
|
||||
|
||||
// Update UI if we have a progress handler registered
|
||||
if (this._planProgressHandler) {
|
||||
this._planProgressHandler({ type: 'plan:progress', data });
|
||||
}
|
||||
|
||||
// Also update the loading display directly for better feedback
|
||||
const titleEl = document.getElementById('planLoadingTitle');
|
||||
const hintEl = document.getElementById('planLoadingHint');
|
||||
|
||||
if (titleEl && data.phase) {
|
||||
const phaseLabels = {
|
||||
'parallel-analysis': 'Running parallel analysis...',
|
||||
'subagent': data.detail || 'Subagent working...',
|
||||
'synthesis': 'Synthesizing results...',
|
||||
'verification': 'Running verification...',
|
||||
};
|
||||
titleEl.textContent = phaseLabels[data.phase] || data.phase;
|
||||
}
|
||||
if (hintEl && data.detail) {
|
||||
hintEl.textContent = data.detail;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setConnectionStatus(status) {
|
||||
@@ -2856,6 +2884,7 @@ class ClaudemanApp {
|
||||
|
||||
async generatePlan() {
|
||||
const config = this.ralphWizardConfig;
|
||||
const isDetailed = config.planDetailLevel === 'detailed';
|
||||
|
||||
// Stop any existing generation first
|
||||
this.stopPlanGeneration();
|
||||
@@ -2869,8 +2898,8 @@ class ClaudemanApp {
|
||||
document.getElementById('planEditor')?.classList.add('hidden');
|
||||
document.getElementById('planGenerationLoading')?.classList.remove('hidden');
|
||||
|
||||
// Animated progress phases
|
||||
const phases = [
|
||||
// Different phases for detailed vs standard generation
|
||||
const standardPhases = [
|
||||
{ time: 0, title: 'Starting Opus 4.5...', hint: 'Initializing deep reasoning model' },
|
||||
{ time: 3, title: 'Analyzing task requirements...', hint: 'Understanding the scope and complexity' },
|
||||
{ time: 8, title: 'Identifying components...', hint: 'Breaking down into modules and features' },
|
||||
@@ -2882,6 +2911,23 @@ class ClaudemanApp {
|
||||
{ time: 90, title: 'Still working...', hint: 'Complex tasks take longer - hang tight!' },
|
||||
];
|
||||
|
||||
const detailedPhases = [
|
||||
{ time: 0, title: 'Spawning analysis subagents...', hint: 'Starting 4 specialist agents in parallel' },
|
||||
{ time: 3, title: 'Requirements Analyst working...', hint: 'Extracting explicit and implicit requirements' },
|
||||
{ time: 6, title: 'Architecture Planner working...', hint: 'Designing modules and interfaces' },
|
||||
{ time: 9, title: 'TDD Specialist working...', hint: 'Planning test-first implementation' },
|
||||
{ time: 12, title: 'Risk Analyst working...', hint: 'Identifying edge cases and blockers' },
|
||||
{ time: 20, title: 'Subagents completing...', hint: 'Collecting analysis results' },
|
||||
{ time: 30, title: 'Synthesizing results...', hint: 'Merging and deduplicating items' },
|
||||
{ time: 40, title: 'Starting verification...', hint: 'Running quality assurance subagent' },
|
||||
{ time: 50, title: 'Assigning priorities...', hint: 'Determining P0/P1/P2 for each task' },
|
||||
{ time: 60, title: 'Checking for gaps...', hint: 'Identifying missing requirements' },
|
||||
{ time: 75, title: 'Finalizing plan...', hint: 'Preparing validated task list' },
|
||||
{ time: 90, title: 'Still working...', hint: 'Complex tasks take longer - hang tight!' },
|
||||
];
|
||||
|
||||
const phases = isDetailed ? detailedPhases : standardPhases;
|
||||
|
||||
// Start elapsed time and phase display
|
||||
this.planLoadingStartTime = Date.now();
|
||||
const timeEl = document.getElementById('planLoadingTime');
|
||||
@@ -2908,14 +2954,42 @@ class ClaudemanApp {
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Listen for real-time progress updates from detailed generation
|
||||
const handlePlanProgress = (event) => {
|
||||
if (event.type === 'plan:progress' && event.data) {
|
||||
const titleEl = document.getElementById('planLoadingTitle');
|
||||
const hintEl = document.getElementById('planLoadingHint');
|
||||
if (titleEl && event.data.phase) {
|
||||
const phaseLabels = {
|
||||
'parallel-analysis': 'Running parallel analysis...',
|
||||
'subagent': event.data.detail || 'Subagent working...',
|
||||
'synthesis': 'Synthesizing results...',
|
||||
'verification': 'Running verification...',
|
||||
};
|
||||
titleEl.textContent = phaseLabels[event.data.phase] || event.data.phase;
|
||||
}
|
||||
if (hintEl && event.data.detail) {
|
||||
hintEl.textContent = event.data.detail;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add SSE listener for detailed mode progress
|
||||
if (isDetailed) {
|
||||
this._planProgressHandler = handlePlanProgress;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/generate-plan', {
|
||||
// Use different endpoint for detailed mode
|
||||
const endpoint = isDetailed ? '/api/generate-plan-detailed' : '/api/generate-plan';
|
||||
const body = isDetailed
|
||||
? { taskDescription: config.taskDescription }
|
||||
: { taskDescription: config.taskDescription, detailLevel: config.planDetailLevel };
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
taskDescription: config.taskDescription,
|
||||
detailLevel: config.planDetailLevel,
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
signal: this.planGenerationAbortController?.signal,
|
||||
});
|
||||
|
||||
@@ -2927,6 +3001,9 @@ class ClaudemanApp {
|
||||
this.planLoadingTimer = null;
|
||||
}
|
||||
|
||||
// Remove progress handler
|
||||
this._planProgressHandler = null;
|
||||
|
||||
if (!data.success) {
|
||||
this.showPlanError(data.error || 'Failed to generate plan');
|
||||
return;
|
||||
@@ -2937,12 +3014,20 @@ class ClaudemanApp {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show "Done!" briefly before transitioning
|
||||
const titleEl = document.getElementById('planLoadingTitle');
|
||||
const hintEl = document.getElementById('planLoadingHint');
|
||||
// Show "Done!" with quality info for detailed mode
|
||||
const doneTitle = document.getElementById('planLoadingTitle');
|
||||
const doneHint = document.getElementById('planLoadingHint');
|
||||
const spinnerEl = document.querySelector('.plan-spinner');
|
||||
if (titleEl) titleEl.textContent = 'Done!';
|
||||
if (hintEl) hintEl.textContent = `Generated ${data.data.items.length} steps`;
|
||||
|
||||
if (doneTitle) doneTitle.textContent = 'Done!';
|
||||
if (doneHint) {
|
||||
if (isDetailed && data.data.metadata?.qualityScore) {
|
||||
const quality = Math.round(data.data.metadata.qualityScore * 100);
|
||||
doneHint.textContent = `Generated ${data.data.items.length} steps (Quality: ${quality}%)`;
|
||||
} else {
|
||||
doneHint.textContent = `Generated ${data.data.items.length} steps`;
|
||||
}
|
||||
}
|
||||
if (spinnerEl) spinnerEl.style.display = 'none';
|
||||
|
||||
// Brief pause to show "Done!" before showing editor
|
||||
@@ -2958,6 +3043,11 @@ class ClaudemanApp {
|
||||
config.skipPlanGeneration = false;
|
||||
config.planCost = data.data.costUsd || 0;
|
||||
|
||||
// Store metadata for detailed mode
|
||||
if (isDetailed && data.data.metadata) {
|
||||
config.planMetadata = data.data.metadata;
|
||||
}
|
||||
|
||||
// Show editor and update detail buttons
|
||||
this.renderPlanEditor();
|
||||
this.updateDetailLevelButtons();
|
||||
@@ -2969,6 +3059,9 @@ class ClaudemanApp {
|
||||
this.planLoadingTimer = null;
|
||||
}
|
||||
|
||||
// Remove progress handler
|
||||
this._planProgressHandler = null;
|
||||
|
||||
// Ignore abort errors (user cancelled, e.g., clicked "Use Existing Plan")
|
||||
if (err.name === 'AbortError') {
|
||||
console.log('Plan generation aborted by user');
|
||||
@@ -2981,14 +3074,39 @@ class ClaudemanApp {
|
||||
}
|
||||
|
||||
setPlanDetail(level) {
|
||||
const previousLevel = this.ralphWizardConfig.planDetailLevel;
|
||||
this.ralphWizardConfig.planDetailLevel = level;
|
||||
this.updateDetailLevelButtons();
|
||||
|
||||
// If plan was already generated and level changed, automatically regenerate
|
||||
if (this.ralphWizardConfig.planGenerated && previousLevel !== level) {
|
||||
const modeLabel = level === 'detailed' ? 'Enhanced (Multi-Agent)' : level === 'brief' ? 'Brief' : 'Standard';
|
||||
console.log(`[Ralph Wizard] Plan mode changed to ${modeLabel}, regenerating...`);
|
||||
|
||||
// Clear current plan and regenerate
|
||||
this.ralphWizardConfig.generatedPlan = null;
|
||||
this.ralphWizardConfig.planGenerated = false;
|
||||
this.ralphWizardConfig.planMetadata = null;
|
||||
|
||||
// Trigger regeneration with visual feedback
|
||||
this.generatePlan();
|
||||
}
|
||||
}
|
||||
|
||||
updateDetailLevelButtons() {
|
||||
const level = this.ralphWizardConfig.planDetailLevel;
|
||||
document.querySelectorAll('.plan-detail-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.detail === level);
|
||||
const isActive = btn.dataset.detail === level;
|
||||
btn.classList.toggle('active', isActive);
|
||||
|
||||
// Add visual indicator for enhanced mode
|
||||
if (btn.dataset.detail === 'detailed') {
|
||||
btn.title = 'Enhanced: Uses 4 parallel subagents + verification (slower but more thorough)';
|
||||
} else if (btn.dataset.detail === 'standard') {
|
||||
btn.title = 'Standard: Single-pass generation with Opus 4.5';
|
||||
} else {
|
||||
btn.title = 'Brief: High-level milestones only';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3015,11 +3133,48 @@ class ClaudemanApp {
|
||||
list.innerHTML = '';
|
||||
const items = this.ralphWizardConfig.generatedPlan || [];
|
||||
const cost = this.ralphWizardConfig.planCost || 0;
|
||||
const metadata = this.ralphWizardConfig.planMetadata;
|
||||
|
||||
// Update header with item count and cost
|
||||
// Update header with item count, cost, and quality score if available
|
||||
const statsEl = document.getElementById('planStats');
|
||||
if (statsEl) {
|
||||
statsEl.textContent = `${items.length} steps · $${cost.toFixed(3)}`;
|
||||
let statsText = `${items.length} steps · $${cost.toFixed(3)}`;
|
||||
|
||||
// Show quality score and source info for detailed mode
|
||||
if (metadata) {
|
||||
const quality = Math.round(metadata.qualityScore * 100);
|
||||
const qualityClass = quality >= 80 ? 'quality-high' : quality >= 60 ? 'quality-medium' : 'quality-low';
|
||||
statsText = `<span class="${qualityClass}">${quality}% quality</span> · ${items.length} steps · $${cost.toFixed(3)}`;
|
||||
|
||||
// Add subagent breakdown if available
|
||||
if (metadata.synthesisStats?.sourceBreakdown) {
|
||||
const sources = metadata.synthesisStats.sourceBreakdown;
|
||||
const sourceList = Object.entries(sources)
|
||||
.map(([src, count]) => `${src}: ${count}`)
|
||||
.join(', ');
|
||||
statsText += ` · Sources: ${sourceList}`;
|
||||
}
|
||||
}
|
||||
|
||||
statsEl.innerHTML = statsText;
|
||||
}
|
||||
|
||||
// Show warnings/gaps if present from verification
|
||||
const warningsEl = document.getElementById('planWarnings');
|
||||
if (warningsEl) {
|
||||
if (metadata?.verificationWarnings?.length > 0 || metadata?.verificationGaps?.length > 0) {
|
||||
let warningsHtml = '';
|
||||
if (metadata.verificationGaps?.length > 0) {
|
||||
warningsHtml += `<div class="plan-gaps"><strong>Gaps identified:</strong> ${metadata.verificationGaps.join('; ')}</div>`;
|
||||
}
|
||||
if (metadata.verificationWarnings?.length > 0) {
|
||||
warningsHtml += `<div class="plan-warnings"><strong>Warnings:</strong> ${metadata.verificationWarnings.join('; ')}</div>`;
|
||||
}
|
||||
warningsEl.innerHTML = warningsHtml;
|
||||
warningsEl.classList.remove('hidden');
|
||||
} else {
|
||||
warningsEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
items.forEach((item, index) => {
|
||||
|
||||
@@ -1033,14 +1033,15 @@
|
||||
<div class="plan-option-group">
|
||||
<label>Detail:</label>
|
||||
<div class="plan-detail-btns">
|
||||
<button type="button" class="plan-detail-btn" data-detail="brief" onclick="app.setPlanDetail('brief')">Brief</button>
|
||||
<button type="button" class="plan-detail-btn active" data-detail="standard" onclick="app.setPlanDetail('standard')">Standard</button>
|
||||
<button type="button" class="plan-detail-btn" data-detail="detailed" onclick="app.setPlanDetail('detailed')">Detailed</button>
|
||||
<button type="button" class="plan-detail-btn" data-detail="brief" onclick="app.setPlanDetail('brief')" title="High-level milestones only">Brief</button>
|
||||
<button type="button" class="plan-detail-btn active" data-detail="standard" onclick="app.setPlanDetail('standard')" title="Single-pass generation with Opus 4.5">Standard</button>
|
||||
<button type="button" class="plan-detail-btn" data-detail="detailed" onclick="app.setPlanDetail('detailed')" title="Enhanced: 4 parallel subagents + verification (slower but more thorough)">Enhanced</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.regeneratePlan()">Regenerate</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="planWarnings" class="hidden"></div>
|
||||
<div id="planItemsList" class="plan-items-list"></div>
|
||||
<div class="plan-editor-footer">
|
||||
<button class="btn-toolbar btn-sm" onclick="app.addPlanItem()">+ Add Step</button>
|
||||
|
||||
@@ -5718,6 +5718,49 @@ kbd {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Quality score indicators for detailed plan mode */
|
||||
.quality-high {
|
||||
color: #4ade80;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quality-medium {
|
||||
color: #fbbf24;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quality-low {
|
||||
color: #f87171;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mode-changed {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Plan warnings and gaps display */
|
||||
#planWarnings {
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
#planWarnings.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.plan-gaps {
|
||||
color: #f87171;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.plan-warnings {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
/* Textarea with AI Assist button */
|
||||
.textarea-with-assist {
|
||||
position: relative;
|
||||
|
||||
@@ -34,6 +34,7 @@ import { TranscriptWatcher } from '../transcript-watcher.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { createRequire } from 'node:module';
|
||||
import { RunSummaryTracker } from '../run-summary.js';
|
||||
import { PlanOrchestrator, type DetailedPlanResult } from '../plan-orchestrator.js';
|
||||
|
||||
// Load version from package.json
|
||||
const require = createRequire(import.meta.url);
|
||||
@@ -2387,6 +2388,54 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
}
|
||||
});
|
||||
|
||||
// Generate detailed implementation plan using subagent orchestration
|
||||
// This spawns multiple specialist subagents in parallel for thorough analysis
|
||||
this.app.post('/api/generate-plan-detailed', async (req): Promise<ApiResponse> => {
|
||||
const { taskDescription } = req.body as { taskDescription: string };
|
||||
|
||||
if (!taskDescription || typeof taskDescription !== 'string') {
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Task description is required');
|
||||
}
|
||||
|
||||
if (taskDescription.length > 10000) {
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Task description too long (max 10000 chars)');
|
||||
}
|
||||
|
||||
const orchestrator = new PlanOrchestrator(this.screenManager, process.cwd());
|
||||
|
||||
// Track progress for SSE updates
|
||||
const progressUpdates: Array<{ phase: string; detail: string; timestamp: number }> = [];
|
||||
const onProgress = (phase: string, detail: string) => {
|
||||
const update = { phase, detail, timestamp: Date.now() };
|
||||
progressUpdates.push(update);
|
||||
// Broadcast progress to connected clients
|
||||
this.broadcast('plan:progress', update);
|
||||
};
|
||||
|
||||
try {
|
||||
const result: DetailedPlanResult = await orchestrator.generateDetailedPlan(
|
||||
taskDescription,
|
||||
onProgress
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, result.error || 'Plan generation failed');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
items: result.items,
|
||||
costUsd: result.costUsd,
|
||||
metadata: result.metadata,
|
||||
progressLog: progressUpdates,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Detailed plan generation failed: ' + getErrorMessage(err));
|
||||
}
|
||||
});
|
||||
|
||||
// ============ App Settings Endpoints ============
|
||||
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user