mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
chore: bump version to 0.1406
This commit is contained in:
@@ -16,7 +16,7 @@ When user says "COM":
|
||||
1. Increment version in BOTH `package.json` AND `CLAUDE.md`
|
||||
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
|
||||
|
||||
**Version**: 0.1405 (must match `package.json`)
|
||||
**Version**: 0.1406 (must match `package.json`)
|
||||
|
||||
## Project Overview
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claudeman",
|
||||
"version": "0.1405",
|
||||
"version": "0.1406",
|
||||
"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",
|
||||
|
||||
+309
-55
@@ -245,13 +245,77 @@ export interface FinalReviewResult {
|
||||
|
||||
export type SubagentCallback = (event: PlanSubagentEvent) => void;
|
||||
|
||||
// ============================================================================
|
||||
// JSON Repair Helper
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Attempt to parse JSON with automatic repair for common LLM output issues.
|
||||
* Handles: trailing commas, unescaped newlines in strings, truncated output.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function tryParseJSON(jsonString: string): { success: boolean; data?: any; error?: string } {
|
||||
// First, try direct parse
|
||||
try {
|
||||
return { success: true, data: JSON.parse(jsonString) };
|
||||
} catch (firstError) {
|
||||
// Try repairs
|
||||
let repaired = jsonString;
|
||||
|
||||
// 1. Remove trailing commas before ] or }
|
||||
repaired = repaired.replace(/,(\s*[\]}])/g, '$1');
|
||||
|
||||
// 2. Fix unescaped newlines in strings (common LLM issue)
|
||||
// Match strings and escape newlines within them
|
||||
repaired = repaired.replace(/"([^"\\]|\\.)*"/g, (match) => {
|
||||
return match.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
|
||||
});
|
||||
|
||||
// 3. Try to close unclosed arrays/objects (truncated output)
|
||||
const openBrackets = (repaired.match(/\[/g) || []).length;
|
||||
const closeBrackets = (repaired.match(/\]/g) || []).length;
|
||||
const openBraces = (repaired.match(/\{/g) || []).length;
|
||||
const closeBraces = (repaired.match(/\}/g) || []).length;
|
||||
|
||||
// Add missing closing brackets
|
||||
for (let i = 0; i < openBraces - closeBraces; i++) {
|
||||
// Find last incomplete object and close it
|
||||
repaired = repaired.replace(/,\s*$/, '') + '}';
|
||||
}
|
||||
for (let i = 0; i < openBrackets - closeBrackets; i++) {
|
||||
repaired = repaired.replace(/,\s*$/, '') + ']';
|
||||
}
|
||||
|
||||
// Try parsing repaired JSON
|
||||
try {
|
||||
return { success: true, data: JSON.parse(repaired) };
|
||||
} catch (secondError) {
|
||||
// 4. Last resort: try to extract valid items from array
|
||||
// Find all complete objects in the array
|
||||
const objectMatches = jsonString.match(/\{[^{}]*\}/g);
|
||||
if (objectMatches && objectMatches.length > 0) {
|
||||
try {
|
||||
const items = objectMatches.map(obj => JSON.parse(obj));
|
||||
console.warn(`[JSON Repair] Extracted ${items.length} items from malformed array`);
|
||||
return { success: true, data: items };
|
||||
} catch {
|
||||
// Give up
|
||||
}
|
||||
}
|
||||
|
||||
const errMsg = firstError instanceof Error ? firstError.message : String(firstError);
|
||||
return { success: false, error: errMsg };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
const RESEARCH_TIMEOUT_MS = 360000; // 6 minutes for research (may need web search)
|
||||
const SUBAGENT_TIMEOUT_MS = 300000; // 5 minutes per subagent (Opus needs time for complex analysis)
|
||||
const VERIFICATION_TIMEOUT_MS = 480000; // 8 minutes for verification (Opus + large plans)
|
||||
const RESEARCH_TIMEOUT_MS = 600000; // 10 minutes for research (may need extensive web search)
|
||||
const SUBAGENT_TIMEOUT_MS = 480000; // 8 minutes per subagent (Opus needs time for complex analysis)
|
||||
const VERIFICATION_TIMEOUT_MS = 600000; // 10 minutes for verification (Opus + large plans)
|
||||
const MODEL_RESEARCH = 'opus'; // Best model for research (needs reasoning for web search)
|
||||
const MODEL_ANALYSIS = 'opus'; // Best model for thorough analysis
|
||||
const MODEL_VERIFICATION = 'opus'; // Best model for verification
|
||||
@@ -1245,10 +1309,24 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
|
||||
onProgress?.('research', 'Gathering external resources and codebase context...');
|
||||
|
||||
const { result } = await Promise.race([
|
||||
session.runPrompt(prompt, { model: MODEL_RESEARCH }),
|
||||
this.timeout(RESEARCH_TIMEOUT_MS),
|
||||
]);
|
||||
// Periodic progress updates showing elapsed time
|
||||
const progressInterval = setInterval(() => {
|
||||
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
||||
const timeoutSec = Math.floor(RESEARCH_TIMEOUT_MS / 1000);
|
||||
onProgress?.('research', `Research agent working... (${elapsedSec}s / ${timeoutSec}s timeout)`);
|
||||
console.log(`[PlanOrchestrator] Research agent: ${elapsedSec}s elapsed (timeout: ${timeoutSec}s)`);
|
||||
}, 30000); // Update every 30 seconds
|
||||
|
||||
let result: string;
|
||||
try {
|
||||
const response = await Promise.race([
|
||||
session.runPrompt(prompt, { model: MODEL_RESEARCH }),
|
||||
this.timeoutWithContext(RESEARCH_TIMEOUT_MS, 'Research agent', startTime),
|
||||
]);
|
||||
result = response.result;
|
||||
} finally {
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
|
||||
// Check if cancelled during execution
|
||||
if (this.cancelled) {
|
||||
@@ -1264,7 +1342,7 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
return { ...defaultResult, error: 'Cancelled', durationMs: Date.now() - startTime };
|
||||
}
|
||||
|
||||
// Parse JSON from result
|
||||
// Parse JSON from result with repair for common LLM issues
|
||||
const jsonMatch = result.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
console.warn('[PlanOrchestrator] Research agent returned no JSON');
|
||||
@@ -1280,7 +1358,22 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
return { ...defaultResult, durationMs: Date.now() - startTime };
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
const parseResult = tryParseJSON(jsonMatch[0]);
|
||||
if (!parseResult.success) {
|
||||
console.warn('[PlanOrchestrator] Research agent JSON parse failed:', parseResult.error);
|
||||
onSubagent?.({
|
||||
type: 'completed',
|
||||
agentId,
|
||||
agentType: 'research',
|
||||
model: MODEL_RESEARCH,
|
||||
status: 'completed',
|
||||
detail: 'Research completed with parse issues',
|
||||
durationMs: Date.now() - startTime,
|
||||
});
|
||||
return { ...defaultResult, error: parseResult.error, durationMs: Date.now() - startTime };
|
||||
}
|
||||
|
||||
const parsed = parseResult.data;
|
||||
|
||||
// Parse external resources
|
||||
const externalResources = (Array.isArray(parsed.externalResources) ? parsed.externalResources : []).map((r: Record<string, unknown>) => ({
|
||||
@@ -1476,30 +1569,64 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
onProgress?.('review-injection', `Added ${reviewsAdded} auto-review task(s)`);
|
||||
}
|
||||
|
||||
// Phase 5: Execution Optimization for Claude Code
|
||||
onProgress?.('execution-optimization', 'Running execution optimizer for Claude Code...');
|
||||
const executionResult = await this.runExecutionOptimizer(
|
||||
taskDescription,
|
||||
planWithReviews,
|
||||
onProgress,
|
||||
onSubagent
|
||||
);
|
||||
|
||||
totalCost += 0.008; // Execution optimizer cost estimate
|
||||
// Phase 5: Execution Optimization for Claude Code (optional - failures don't block plan)
|
||||
let executionResult: Awaited<ReturnType<typeof this.runExecutionOptimizer>>;
|
||||
try {
|
||||
onProgress?.('execution-optimization', 'Running execution optimizer for Claude Code...');
|
||||
executionResult = await this.runExecutionOptimizer(
|
||||
taskDescription,
|
||||
planWithReviews,
|
||||
onProgress,
|
||||
onSubagent
|
||||
);
|
||||
totalCost += 0.008; // Execution optimizer cost estimate
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
console.warn('[PlanOrchestrator] Execution optimizer failed (continuing without):', errMsg);
|
||||
onProgress?.('execution-optimization', `Skipped (${errMsg.slice(0, 50)})`);
|
||||
executionResult = {
|
||||
success: false,
|
||||
optimizedPlan: planWithReviews,
|
||||
parallelGroups: [],
|
||||
executionStrategy: {
|
||||
totalParallelGroups: 0,
|
||||
sequentialBlockers: [],
|
||||
freshContextPoints: [],
|
||||
estimatedTotalTokens: planWithReviews.length * 20000,
|
||||
estimatedAgentSpawns: Math.ceil(planWithReviews.length / 3),
|
||||
criticalPath: [],
|
||||
optimizationNotes: [`Skipped due to error: ${errMsg}`],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Use optimized plan if successful, otherwise fall back to review plan
|
||||
const optimizedPlan = executionResult.success ? executionResult.optimizedPlan : planWithReviews;
|
||||
|
||||
// Phase 6: Final Review - Holistic validation
|
||||
onProgress?.('final-review', 'Running final review for holistic validation...');
|
||||
const finalReviewResult = await this.runFinalReview(
|
||||
taskDescription,
|
||||
optimizedPlan,
|
||||
onProgress,
|
||||
onSubagent
|
||||
);
|
||||
|
||||
totalCost += 0.01; // Final review cost estimate
|
||||
// Phase 6: Final Review - Holistic validation (optional - failures don't block plan)
|
||||
let finalReviewResult: FinalReviewResult;
|
||||
try {
|
||||
onProgress?.('final-review', 'Running final review for holistic validation...');
|
||||
finalReviewResult = await this.runFinalReview(
|
||||
taskDescription,
|
||||
optimizedPlan,
|
||||
onProgress,
|
||||
onSubagent
|
||||
);
|
||||
totalCost += 0.01; // Final review cost estimate
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
console.warn('[PlanOrchestrator] Final review failed (continuing without):', errMsg);
|
||||
onProgress?.('final-review', `Skipped (${errMsg.slice(0, 50)})`);
|
||||
finalReviewResult = {
|
||||
overallAssessment: 'ready',
|
||||
scores: { logic: 0.8, completeness: 0.8, coherence: 0.8, feasibility: 0.8, overall: 0.8 },
|
||||
summary: `Review skipped: ${errMsg}`,
|
||||
issues: [],
|
||||
missingTasks: [],
|
||||
recommendations: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Apply any missing tasks from final review
|
||||
let finalPlan = optimizedPlan;
|
||||
@@ -1620,10 +1747,31 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
try {
|
||||
onProgress?.('subagent', `Running ${agentType} analysis...`);
|
||||
|
||||
const { result } = await Promise.race([
|
||||
session.runPrompt(prompt, { model: MODEL_ANALYSIS }),
|
||||
this.timeout(SUBAGENT_TIMEOUT_MS),
|
||||
]);
|
||||
// Periodic progress updates showing elapsed time
|
||||
const progressInterval = setInterval(() => {
|
||||
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
||||
const timeoutSec = Math.floor(SUBAGENT_TIMEOUT_MS / 1000);
|
||||
console.log(`[PlanOrchestrator] ${agentType} agent: ${elapsedSec}s elapsed (timeout: ${timeoutSec}s)`);
|
||||
onSubagent?.({
|
||||
type: 'progress',
|
||||
agentId,
|
||||
agentType,
|
||||
model: MODEL_ANALYSIS,
|
||||
status: 'running',
|
||||
detail: `${agentType} analysis... (${elapsedSec}s / ${timeoutSec}s)`,
|
||||
});
|
||||
}, 30000); // Update every 30 seconds
|
||||
|
||||
let result: string;
|
||||
try {
|
||||
const response = await Promise.race([
|
||||
session.runPrompt(prompt, { model: MODEL_ANALYSIS }),
|
||||
this.timeoutWithContext(SUBAGENT_TIMEOUT_MS, `${agentType} agent`, startTime),
|
||||
]);
|
||||
result = response.result;
|
||||
} finally {
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
|
||||
// Check if cancelled during execution
|
||||
if (this.cancelled) {
|
||||
@@ -1645,7 +1793,7 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
};
|
||||
}
|
||||
|
||||
// Parse JSON from result
|
||||
// Parse JSON from result with repair for common LLM issues
|
||||
const jsonMatch = result.match(/\[[\s\S]*\]/);
|
||||
if (!jsonMatch) {
|
||||
onSubagent?.({
|
||||
@@ -1666,7 +1814,28 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
const parseResult = tryParseJSON(jsonMatch[0]);
|
||||
if (!parseResult.success) {
|
||||
console.error(`[PlanOrchestrator] ${agentType} JSON parse failed:`, parseResult.error);
|
||||
onSubagent?.({
|
||||
type: 'failed',
|
||||
agentId,
|
||||
agentType,
|
||||
model: MODEL_ANALYSIS,
|
||||
status: 'failed',
|
||||
error: `JSON parse error: ${parseResult.error}`,
|
||||
durationMs: Date.now() - startTime,
|
||||
});
|
||||
return {
|
||||
agentType,
|
||||
items: [],
|
||||
success: false,
|
||||
error: `JSON parse error: ${parseResult.error}`,
|
||||
durationMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = parseResult.data;
|
||||
if (!Array.isArray(parsed)) {
|
||||
onSubagent?.({
|
||||
type: 'failed',
|
||||
@@ -1930,24 +2099,51 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
|
||||
onProgress?.('verification', 'Validating plan quality...');
|
||||
|
||||
const { result } = await Promise.race([
|
||||
session.runPrompt(prompt, { model: MODEL_VERIFICATION }),
|
||||
this.timeout(VERIFICATION_TIMEOUT_MS),
|
||||
]);
|
||||
// Periodic progress updates showing elapsed time
|
||||
const progressInterval = setInterval(() => {
|
||||
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
||||
const timeoutSec = Math.floor(VERIFICATION_TIMEOUT_MS / 1000);
|
||||
console.log(`[PlanOrchestrator] verification agent: ${elapsedSec}s elapsed (timeout: ${timeoutSec}s)`);
|
||||
onSubagent?.({
|
||||
type: 'progress',
|
||||
agentId,
|
||||
agentType: 'verification',
|
||||
model: MODEL_VERIFICATION,
|
||||
status: 'running',
|
||||
detail: `Verifying plan... (${elapsedSec}s / ${timeoutSec}s)`,
|
||||
});
|
||||
}, 30000); // Update every 30 seconds
|
||||
|
||||
let result: string;
|
||||
try {
|
||||
const response = await Promise.race([
|
||||
session.runPrompt(prompt, { model: MODEL_VERIFICATION }),
|
||||
this.timeoutWithContext(VERIFICATION_TIMEOUT_MS, 'verification agent', startTime),
|
||||
]);
|
||||
result = response.result;
|
||||
} finally {
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
|
||||
// Check if cancelled during execution
|
||||
if (this.cancelled) {
|
||||
return this.fallbackVerification(synthesizedItems);
|
||||
}
|
||||
|
||||
// Parse JSON from result
|
||||
// Parse JSON from result with repair for common LLM issues
|
||||
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 parseResult = tryParseJSON(jsonMatch[0]);
|
||||
if (!parseResult.success) {
|
||||
console.warn('[PlanOrchestrator] Verification JSON parse failed:', parseResult.error);
|
||||
return this.fallbackVerification(synthesizedItems);
|
||||
}
|
||||
|
||||
const parsed = parseResult.data;
|
||||
|
||||
const validatedPlan: PlanItem[] = (parsed.validatedPlan || []).map((item: unknown, idx: number) => {
|
||||
if (typeof item !== 'object' || item === null) {
|
||||
@@ -2169,10 +2365,31 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
|
||||
onProgress?.('execution-optimization', 'Analyzing parallelization opportunities...');
|
||||
|
||||
const { result } = await Promise.race([
|
||||
session.runPrompt(prompt, { model: MODEL_VERIFICATION }),
|
||||
this.timeout(VERIFICATION_TIMEOUT_MS),
|
||||
]);
|
||||
// Periodic progress updates showing elapsed time
|
||||
const progressInterval = setInterval(() => {
|
||||
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
||||
const timeoutSec = Math.floor(VERIFICATION_TIMEOUT_MS / 1000);
|
||||
console.log(`[PlanOrchestrator] execution optimizer: ${elapsedSec}s elapsed (timeout: ${timeoutSec}s)`);
|
||||
onSubagent?.({
|
||||
type: 'progress',
|
||||
agentId,
|
||||
agentType: 'execution',
|
||||
model: MODEL_VERIFICATION,
|
||||
status: 'running',
|
||||
detail: `Optimizing execution... (${elapsedSec}s / ${timeoutSec}s)`,
|
||||
});
|
||||
}, 30000); // Update every 30 seconds
|
||||
|
||||
let result: string;
|
||||
try {
|
||||
const response = await Promise.race([
|
||||
session.runPrompt(prompt, { model: MODEL_VERIFICATION }),
|
||||
this.timeoutWithContext(VERIFICATION_TIMEOUT_MS, 'execution optimizer', startTime),
|
||||
]);
|
||||
result = response.result;
|
||||
} finally {
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
|
||||
// Check if cancelled during execution
|
||||
if (this.cancelled) {
|
||||
@@ -2196,7 +2413,13 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
return defaultResult;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
const parseResult = tryParseJSON(jsonMatch[0]);
|
||||
if (!parseResult.success) {
|
||||
console.warn('[PlanOrchestrator] Execution optimizer JSON parse failed:', parseResult.error);
|
||||
return defaultResult;
|
||||
}
|
||||
|
||||
const parsed = parseResult.data;
|
||||
|
||||
// Parse optimized plan items
|
||||
const optimizedPlan: PlanItem[] = (parsed.optimizedPlan || []).map((item: Record<string, unknown>, idx: number) => {
|
||||
@@ -2370,17 +2593,38 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
|
||||
onProgress?.('final-review', 'Analyzing overall plan coherence...');
|
||||
|
||||
const { result } = await Promise.race([
|
||||
session.runPrompt(prompt, { model: MODEL_VERIFICATION }),
|
||||
this.timeout(VERIFICATION_TIMEOUT_MS),
|
||||
]);
|
||||
// Periodic progress updates showing elapsed time
|
||||
const progressInterval = setInterval(() => {
|
||||
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
||||
const timeoutSec = Math.floor(VERIFICATION_TIMEOUT_MS / 1000);
|
||||
console.log(`[PlanOrchestrator] final review: ${elapsedSec}s elapsed (timeout: ${timeoutSec}s)`);
|
||||
onSubagent?.({
|
||||
type: 'progress',
|
||||
agentId,
|
||||
agentType: 'final-review',
|
||||
model: MODEL_VERIFICATION,
|
||||
status: 'running',
|
||||
detail: `Final review... (${elapsedSec}s / ${timeoutSec}s)`,
|
||||
});
|
||||
}, 30000); // Update every 30 seconds
|
||||
|
||||
let result: string;
|
||||
try {
|
||||
const response = await Promise.race([
|
||||
session.runPrompt(prompt, { model: MODEL_VERIFICATION }),
|
||||
this.timeoutWithContext(VERIFICATION_TIMEOUT_MS, 'final review', startTime),
|
||||
]);
|
||||
result = response.result;
|
||||
} finally {
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
|
||||
// Check if cancelled during execution
|
||||
if (this.cancelled) {
|
||||
return defaultResult;
|
||||
}
|
||||
|
||||
// Parse JSON from result
|
||||
// Parse JSON from result with repair for common LLM issues
|
||||
const jsonMatch = result.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
console.warn('[PlanOrchestrator] Final review returned no JSON, using defaults');
|
||||
@@ -2397,7 +2641,13 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
return defaultResult;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
const parseResult = tryParseJSON(jsonMatch[0]);
|
||||
if (!parseResult.success) {
|
||||
console.warn('[PlanOrchestrator] Final review JSON parse failed:', parseResult.error);
|
||||
return defaultResult;
|
||||
}
|
||||
|
||||
const parsed = parseResult.data;
|
||||
|
||||
// Parse scores
|
||||
const scores = {
|
||||
@@ -2526,11 +2776,15 @@ Use \`/init\` to load this context, then read specific files as needed.
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a timeout promise.
|
||||
* Create a timeout promise with context about what timed out.
|
||||
*/
|
||||
private timeout(ms: number): Promise<never> {
|
||||
private timeoutWithContext(ms: number, context: string, startTime: number): Promise<never> {
|
||||
return new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
|
||||
setTimeout(() => {
|
||||
const elapsedSec = Math.floor((Date.now() - startTime) / 1000);
|
||||
const timeoutSec = Math.floor(ms / 1000);
|
||||
reject(new Error(`${context} timed out after ${elapsedSec}s (limit: ${timeoutSec}s)`));
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+66
-14
@@ -1946,6 +1946,31 @@ class ClaudemanApp {
|
||||
hintEl.textContent = data.detail;
|
||||
}
|
||||
});
|
||||
|
||||
// Plan generation started - store orchestrator ID for cancellation
|
||||
addListener('plan:started', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
console.log('[Plan Started]', data);
|
||||
this.activePlanOrchestratorId = data.orchestratorId;
|
||||
});
|
||||
|
||||
// Plan generation cancelled
|
||||
addListener('plan:cancelled', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
console.log('[Plan Cancelled]', data);
|
||||
if (this.activePlanOrchestratorId === data.orchestratorId) {
|
||||
this.activePlanOrchestratorId = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Plan generation completed
|
||||
addListener('plan:completed', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
console.log('[Plan Completed]', data);
|
||||
if (this.activePlanOrchestratorId === data.orchestratorId) {
|
||||
this.activePlanOrchestratorId = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setConnectionStatus(status) {
|
||||
@@ -2985,18 +3010,23 @@ class ClaudemanApp {
|
||||
];
|
||||
|
||||
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!' },
|
||||
// Phase 0: Research (can take up to 10 minutes for complex tasks)
|
||||
{ time: 0, title: 'Starting research agent...', hint: 'Gathering external resources and codebase context' },
|
||||
{ time: 30, title: 'Research agent working...', hint: 'Searching docs, GitHub repos, and analyzing codebase' },
|
||||
{ time: 60, title: 'Research continuing...', hint: 'Web search and codebase exploration in progress' },
|
||||
{ time: 120, title: 'Research agent deep diving...', hint: 'Complex tasks require thorough research' },
|
||||
{ time: 180, title: 'Research almost complete...', hint: 'Compiling findings and recommendations' },
|
||||
// Phase 1: Parallel Analysis (after research completes)
|
||||
// Note: These times are fallbacks - real-time SSE updates override them
|
||||
{ time: 300, title: 'Spawning analysis subagents...', hint: 'Starting 4 specialist agents in parallel' },
|
||||
{ time: 330, title: 'Subagents analyzing...', hint: 'Requirements, Architecture, Testing, Risk analysts working' },
|
||||
{ time: 400, title: 'Subagents completing...', hint: 'Collecting analysis results' },
|
||||
// Phase 2+: Synthesis and verification
|
||||
{ time: 450, title: 'Synthesizing results...', hint: 'Merging and deduplicating items' },
|
||||
{ time: 500, title: 'Running verification...', hint: 'Quality assurance and priority assignment' },
|
||||
{ time: 550, title: 'Optimizing execution...', hint: 'Planning parallelization for Claude Code' },
|
||||
{ time: 600, title: 'Final review...', hint: 'Holistic validation and gap detection' },
|
||||
{ time: 660, title: 'Still working...', hint: 'Complex tasks take longer - hang tight!' },
|
||||
];
|
||||
|
||||
const phases = isDetailed ? detailedPhases : standardPhases;
|
||||
@@ -3034,10 +3064,14 @@ class ClaudemanApp {
|
||||
const hintEl = document.getElementById('planLoadingHint');
|
||||
if (titleEl && event.data.phase) {
|
||||
const phaseLabels = {
|
||||
'parallel-analysis': 'Running parallel analysis...',
|
||||
'research': 'Research agent working...',
|
||||
'parallel-analysis': 'Spawning analysis subagents...',
|
||||
'subagent': event.data.detail || 'Subagent working...',
|
||||
'synthesis': 'Synthesizing results...',
|
||||
'verification': 'Running verification...',
|
||||
'review-injection': 'Adding review tasks...',
|
||||
'execution-optimization': 'Optimizing for Claude Code...',
|
||||
'final-review': 'Running final review...',
|
||||
};
|
||||
titleEl.textContent = phaseLabels[event.data.phase] || event.data.phase;
|
||||
}
|
||||
@@ -3595,9 +3629,25 @@ class ClaudemanApp {
|
||||
}, 50);
|
||||
}
|
||||
|
||||
cancelPlanGeneration() {
|
||||
async cancelPlanGeneration() {
|
||||
this.stopPlanGeneration();
|
||||
this.planGenerationStopped = true; // Ignore future SSE events
|
||||
|
||||
// Call the cancel API to stop server-side processing
|
||||
if (this.activePlanOrchestratorId) {
|
||||
try {
|
||||
console.log('[Cancel] Sending cancel request for', this.activePlanOrchestratorId);
|
||||
await fetch('/api/cancel-plan-generation', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ orchestratorId: this.activePlanOrchestratorId }),
|
||||
});
|
||||
this.activePlanOrchestratorId = null;
|
||||
} catch (err) {
|
||||
console.error('[Cancel] Failed to cancel plan generation:', err);
|
||||
}
|
||||
}
|
||||
|
||||
this.showToast('Plan generation stopped', 'info');
|
||||
|
||||
// Allow user to proceed without a plan by clicking Next
|
||||
@@ -3783,6 +3833,7 @@ class ClaudemanApp {
|
||||
win.style.zIndex = ++this.planSubagentWindowZIndex;
|
||||
|
||||
const typeLabels = {
|
||||
research: 'Research Agent',
|
||||
requirements: 'Requirements Analyst',
|
||||
architecture: 'Architecture Planner',
|
||||
testing: 'TDD Specialist',
|
||||
@@ -3793,6 +3844,7 @@ class ClaudemanApp {
|
||||
};
|
||||
|
||||
const typeIcons = {
|
||||
research: '🔬',
|
||||
requirements: '📋',
|
||||
architecture: '🏗️',
|
||||
testing: '🧪',
|
||||
|
||||
+46
-25
@@ -307,6 +307,8 @@ export class WebServer extends EventEmitter {
|
||||
private readonly serverStartTime: number = Date.now();
|
||||
// Pending respawn start timers (for cleanup on shutdown)
|
||||
private pendingRespawnStarts: Map<string, NodeJS.Timeout> = new Map();
|
||||
// Active plan orchestrators (for cancellation via API)
|
||||
private activePlanOrchestrators: Map<string, PlanOrchestrator> = new Map();
|
||||
// Grace period before starting restored respawn controllers (2 minutes)
|
||||
private static readonly RESPAWN_RESTORE_GRACE_PERIOD_MS = 2 * 60 * 1000;
|
||||
|
||||
@@ -2445,21 +2447,13 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
|
||||
const orchestrator = new PlanOrchestrator(this.screenManager, process.cwd(), outputDir);
|
||||
|
||||
// Cancel orchestrator if client disconnects (user clicked Stop)
|
||||
// Note: We use the socket's 'close' event, not req.raw.on('close') which fires
|
||||
// when the request body is done being read (immediately after parsing).
|
||||
let clientDisconnected = false;
|
||||
let responseSent = false;
|
||||
const socket = req.raw.socket;
|
||||
const onClose = () => {
|
||||
// Only cancel if response hasn't been sent yet (true disconnection)
|
||||
if (!responseSent) {
|
||||
clientDisconnected = true;
|
||||
console.log('[API] Client disconnected during plan generation, cancelling...');
|
||||
orchestrator.cancel().catch(() => { /* ignore */ });
|
||||
}
|
||||
};
|
||||
socket.on('close', onClose);
|
||||
// Store orchestrator for potential cancellation via API (not on disconnect)
|
||||
// Plan generation continues even if browser disconnects - only explicit cancel stops it
|
||||
const orchestratorId = `plan-${Date.now()}`;
|
||||
this.activePlanOrchestrators.set(orchestratorId, orchestrator);
|
||||
|
||||
// Broadcast the orchestrator ID so frontend can cancel if needed
|
||||
this.broadcast('plan:started', { orchestratorId });
|
||||
|
||||
// Track progress for SSE updates
|
||||
const progressUpdates: Array<{ phase: string; detail: string; timestamp: number }> = [];
|
||||
@@ -2492,14 +2486,9 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
onSubagent
|
||||
);
|
||||
|
||||
// Mark response as being sent and clean up listener
|
||||
responseSent = true;
|
||||
socket.off('close', onClose);
|
||||
|
||||
// If client disconnected, don't bother returning response
|
||||
if (clientDisconnected) {
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Cancelled by client');
|
||||
}
|
||||
// Clean up orchestrator from active map
|
||||
this.activePlanOrchestrators.delete(orchestratorId);
|
||||
this.broadcast('plan:completed', { orchestratorId, success: result.success });
|
||||
|
||||
if (!result.success) {
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, result.error || 'Plan generation failed');
|
||||
@@ -2512,15 +2501,47 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
|
||||
costUsd: result.costUsd,
|
||||
metadata: result.metadata,
|
||||
progressLog: progressUpdates,
|
||||
orchestratorId,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
responseSent = true;
|
||||
socket.off('close', onClose);
|
||||
// Clean up on error too
|
||||
this.activePlanOrchestrators.delete(orchestratorId);
|
||||
this.broadcast('plan:completed', { orchestratorId, success: false, error: getErrorMessage(err) });
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Detailed plan generation failed: ' + getErrorMessage(err));
|
||||
}
|
||||
});
|
||||
|
||||
// Cancel active plan generation
|
||||
this.app.post('/api/cancel-plan-generation', async (req): Promise<ApiResponse> => {
|
||||
const { orchestratorId } = req.body as { orchestratorId?: string };
|
||||
|
||||
// If specific orchestrator ID provided, cancel just that one
|
||||
if (orchestratorId) {
|
||||
const orchestrator = this.activePlanOrchestrators.get(orchestratorId);
|
||||
if (!orchestrator) {
|
||||
return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Plan generation not found or already completed');
|
||||
}
|
||||
console.log(`[API] Cancelling plan generation ${orchestratorId}`);
|
||||
await orchestrator.cancel();
|
||||
this.activePlanOrchestrators.delete(orchestratorId);
|
||||
this.broadcast('plan:cancelled', { orchestratorId });
|
||||
return { success: true, data: { cancelled: orchestratorId } };
|
||||
}
|
||||
|
||||
// Otherwise cancel all active plan generations
|
||||
const cancelled: string[] = [];
|
||||
for (const [id, orchestrator] of this.activePlanOrchestrators) {
|
||||
console.log(`[API] Cancelling plan generation ${id}`);
|
||||
await orchestrator.cancel();
|
||||
cancelled.push(id);
|
||||
this.broadcast('plan:cancelled', { orchestratorId: id });
|
||||
}
|
||||
this.activePlanOrchestrators.clear();
|
||||
|
||||
return { success: true, data: { cancelled } };
|
||||
});
|
||||
|
||||
// ============ Plan Management Endpoints ============
|
||||
// These endpoints support runtime plan adaptation with checkpoints, failure tracking, and versioning
|
||||
|
||||
|
||||
Reference in New Issue
Block a user