feat: add CCW Loop System for automated iterative workflow execution

Implements a complete loop execution system with multi-loop parallel support,
dashboard monitoring, and comprehensive security validation.

Core features:
- Loop orchestration engine (loop-manager, loop-state-manager)
- Multi-loop parallel execution with independent state management
- REST API endpoints for loop control (pause, resume, stop, retry)
- WebSocket real-time status updates
- Dashboard Loop Monitor view with live updates
- Security: path traversal protection and sandboxed JavaScript evaluation

Test coverage:
- 42 comprehensive tests covering multi-loop, API, WebSocket, security
- Security validation for success_condition injection attacks
- Edge case handling and end-to-end workflow tests
This commit is contained in:
catlog22
2026-01-21 22:55:24 +08:00
parent 64e064e775
commit d9f1d14d5e
28 changed files with 5912 additions and 17 deletions

View File

@@ -610,6 +610,19 @@ export function updateClaudeDefaultTool(
return settings;
}
/**
* Get the default tool from config
* Returns the configured defaultTool or 'gemini' as fallback
*/
export function getDefaultTool(projectDir: string): string {
try {
const settings = loadClaudeCliSettings(projectDir);
return settings.defaultTool || 'gemini';
} catch {
return 'gemini';
}
}
/**
* Add API endpoint as a tool with type: 'api-endpoint'
* Usage: --tool <name> or --tool custom --model <id>
@@ -943,3 +956,133 @@ export function getFullConfigResponse(projectDir: string): {
predefinedModels: { ...PREDEFINED_MODELS }
};
}
// ========== Tool Detection & Sync Functions ==========
/**
* Sync builtin tools availability with cli-tools.json
*
* For builtin tools (gemini, qwen, codex, claude, opencode):
* - Checks actual tool availability using system PATH
* - Updates enabled status based on actual availability
*
* For non-builtin tools (cli-wrapper, api-endpoint):
* - Leaves them unchanged as they have different availability mechanisms
*
* @returns Updated config and sync results
*/
export async function syncBuiltinToolsAvailability(projectDir: string): Promise<{
config: ClaudeCliToolsConfig;
changes: {
enabled: string[]; // Tools that were enabled
disabled: string[]; // Tools that were disabled
unchanged: string[]; // Tools that stayed the same
};
}> {
// Import getCliToolsStatus dynamically to avoid circular dependency
const { getCliToolsStatus } = await import('./cli-executor.js');
// Get actual tool availability
const actualStatus = await getCliToolsStatus();
// Load current config
const config = loadClaudeCliTools(projectDir);
const changes = {
enabled: [] as string[],
disabled: [] as string[],
unchanged: [] as string[]
};
// Builtin tools that need sync
const builtinTools = ['gemini', 'qwen', 'codex', 'claude', 'opencode'];
for (const toolName of builtinTools) {
const isAvailable = actualStatus[toolName]?.available ?? false;
const currentConfig = config.tools[toolName];
const wasEnabled = currentConfig?.enabled ?? true;
// Update based on actual availability
if (isAvailable && !wasEnabled) {
// Tool exists but was disabled - enable it
if (!currentConfig) {
config.tools[toolName] = {
enabled: true,
primaryModel: DEFAULT_TOOLS_CONFIG.tools[toolName]?.primaryModel || '',
secondaryModel: DEFAULT_TOOLS_CONFIG.tools[toolName]?.secondaryModel || '',
tags: [],
type: 'builtin'
};
} else {
currentConfig.enabled = true;
}
changes.enabled.push(toolName);
} else if (!isAvailable && wasEnabled) {
// Tool doesn't exist but was enabled - disable it
if (currentConfig) {
currentConfig.enabled = false;
}
changes.disabled.push(toolName);
} else {
// No change needed
changes.unchanged.push(toolName);
}
}
// Save updated config
saveClaudeCliTools(projectDir, config);
console.log('[claude-cli-tools] Synced builtin tools availability:', {
enabled: changes.enabled,
disabled: changes.disabled,
unchanged: changes.unchanged
});
return { config, changes };
}
/**
* Get sync status report without actually modifying config
*
* @returns Report showing what would change if sync were run
*/
export async function getBuiltinToolsSyncReport(projectDir: string): Promise<{
current: Record<string, { available: boolean; enabled: boolean }>;
recommended: Record<string, { shouldEnable: boolean; reason: string }>;
}> {
// Import getCliToolsStatus dynamically to avoid circular dependency
const { getCliToolsStatus } = await import('./cli-executor.js');
// Get actual tool availability
const actualStatus = await getCliToolsStatus();
// Load current config
const config = loadClaudeCliTools(projectDir);
const builtinTools = ['gemini', 'qwen', 'codex', 'claude', 'opencode'];
const current: Record<string, { available: boolean; enabled: boolean }> = {};
const recommended: Record<string, { shouldEnable: boolean; reason: string }> = {};
for (const toolName of builtinTools) {
const isAvailable = actualStatus[toolName]?.available ?? false;
const isEnabled = config.tools[toolName]?.enabled ?? true;
current[toolName] = {
available: isAvailable,
enabled: isEnabled
};
if (isAvailable && !isEnabled) {
recommended[toolName] = {
shouldEnable: true,
reason: 'Tool is installed but disabled in config'
};
} else if (!isAvailable && isEnabled) {
recommended[toolName] = {
shouldEnable: false,
reason: 'Tool is not installed but enabled in config'
};
}
}
return { current, recommended };
}