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

@@ -66,6 +66,27 @@
color: hsl(var(--muted-foreground));
}
/* CLI status actions container */
.cli-status-actions {
display: flex;
align-items: center;
gap: 0.375rem;
}
/* Spin animation for sync icon */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.spin {
animation: spin 1s linear infinite;
}
.cli-tools-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));

File diff suppressed because it is too large Load Diff

View File

@@ -771,9 +771,14 @@ function renderCliStatus() {
container.innerHTML = `
<div class="cli-status-header">
<h3><i data-lucide="terminal" class="w-4 h-4"></i> CLI Tools</h3>
<button class="btn-icon" onclick="refreshAllCliStatus()" title="Refresh">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
</button>
<div class="cli-status-actions">
<button class="btn-icon" onclick="syncBuiltinTools()" title="Sync tool availability with installed CLI tools">
<i data-lucide="sync" class="w-4 h-4"></i>
</button>
<button class="btn-icon" onclick="refreshAllCliStatus()" title="Refresh">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
</button>
</div>
</div>
${ccwInstallHtml}
<div class="cli-tools-grid">
@@ -825,6 +830,62 @@ function setPromptFormat(format) {
showRefreshToast(`Prompt format set to ${format.toUpperCase()}`, 'success');
}
/**
* Sync builtin tools availability with installed CLI tools
* Checks system PATH and updates cli-tools.json accordingly
*/
async function syncBuiltinTools() {
const syncButton = document.querySelector('[onclick="syncBuiltinTools()"]');
if (syncButton) {
syncButton.disabled = true;
const icon = syncButton.querySelector('i');
if (icon) icon.classList.add('spin');
}
try {
const response = await csrfFetch('/api/cli/settings/sync-tools', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
throw new Error('Sync failed');
}
const result = await response.json();
// Reload the config after sync
await loadCliToolsConfig();
await loadAllStatuses();
renderCliStatus();
// Show summary of changes
const { enabled, disabled, unchanged } = result.changes;
let message = 'Tools synced: ';
const parts = [];
if (enabled.length > 0) parts.push(`${enabled.join(', ')} enabled`);
if (disabled.length > 0) parts.push(`${disabled.join(', ')} disabled`);
if (unchanged.length > 0) parts.push(`${unchanged.length} unchanged`);
message += parts.join(', ');
showRefreshToast(message, 'success');
// Also invalidate the CLI tool cache to ensure fresh checks
if (window.cacheManager) {
window.cacheManager.delete('cli-tools-status');
}
} catch (err) {
console.error('Failed to sync tools:', err);
showRefreshToast('Failed to sync tools: ' + (err.message || String(err)), 'error');
} finally {
if (syncButton) {
syncButton.disabled = false;
const icon = syncButton.querySelector('i');
if (icon) icon.classList.remove('spin');
}
}
}
function setSmartContextEnabled(enabled) {
smartContextEnabled = enabled;
localStorage.setItem('ccw-smart-context', enabled.toString());

View File

@@ -183,6 +183,14 @@ function initNavigation() {
} else {
console.error('renderIssueDiscovery not defined - please refresh the page');
}
} else if (currentView === 'loop-monitor') {
if (typeof renderLoopMonitor === 'function') {
renderLoopMonitor();
// Register destroy function for cleanup
currentViewDestroy = window.destroyLoopMonitor;
} else {
console.error('renderLoopMonitor not defined - please refresh the page');
}
}
});
});
@@ -231,6 +239,8 @@ function updateContentTitle() {
titleEl.textContent = t('title.issueManager');
} else if (currentView === 'issue-discovery') {
titleEl.textContent = t('title.issueDiscovery');
} else if (currentView === 'loop-monitor') {
titleEl.textContent = t('title.loopMonitor') || 'Loop Monitor';
} else if (currentView === 'liteTasks') {
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions'), 'multi-cli-plan': t('title.multiCliPlanSessions') || 'Multi-CLI Plan Sessions' };
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');

View File

@@ -87,6 +87,10 @@ const i18n = {
'nav.liteFix': 'Lite Fix',
'nav.multiCliPlan': 'Multi-CLI Plan',
// Sidebar - Loops section
'nav.loops': 'Loops',
'nav.loopMonitor': 'Monitor',
// Sidebar - MCP section
'nav.mcpServers': 'MCP Servers',
'nav.manage': 'Manage',
@@ -2144,6 +2148,51 @@ const i18n = {
'title.issueManager': 'Issue Manager',
'title.issueDiscovery': 'Issue Discovery',
// Loop Monitor
'title.loopMonitor': 'Loop Monitor',
'loop.title': 'Loop Monitor',
'loop.status.created': 'Created',
'loop.status.running': 'Running',
'loop.status.paused': 'Paused',
'loop.status.completed': 'Completed',
'loop.status.failed': 'Failed',
'loop.tabs.timeline': 'Timeline',
'loop.tabs.logs': 'Logs',
'loop.tabs.variables': 'Variables',
'loop.buttons.pause': 'Pause',
'loop.buttons.resume': 'Resume',
'loop.buttons.stop': 'Stop',
'loop.buttons.retry': 'Retry',
'loop.buttons.newLoop': 'New Loop',
'loop.empty': 'No active loops',
'loop.metric.iteration': 'Iteration',
'loop.metric.step': 'Step',
'loop.metric.duration': 'Duration',
'loop.task.id': 'Task',
'loop.created': 'Created',
'loop.updated': 'Updated',
'loop.progress': 'Progress',
'loop.cliSequence': 'CLI Sequence',
'loop.stateVariables': 'State Variables',
'loop.executionHistory': 'Execution History',
'loop.failureReason': 'Failure Reason',
'loop.noLoopsFound': 'No loops found',
'loop.selectLoop': 'Select a loop to view details',
'loop.tasks': 'Tasks',
'loop.createTaskTitle': 'Create Loop Task',
'loop.loopsCount': 'loops',
'loop.paused': 'Loop paused',
'loop.resumed': 'Loop resumed',
'loop.stopped': 'Loop stopped',
'loop.startedSuccess': 'Loop started',
'loop.taskDescription': 'Description',
'loop.maxIterations': 'Max Iterations',
'loop.errorPolicy': 'Error Policy',
'loop.pauseOnError': 'Pause on error',
'loop.retryAutomatically': 'Retry automatically',
'loop.failImmediate': 'Fail immediately',
'loop.successCondition': 'Success Condition',
// Issue Discovery
'discovery.title': 'Issue Discovery',
'discovery.description': 'Discover potential issues from multiple perspectives',
@@ -2438,6 +2487,10 @@ const i18n = {
'nav.liteFix': '轻量修复',
'nav.multiCliPlan': '多CLI规划',
// Sidebar - Loops section
'nav.loops': '循环',
'nav.loopMonitor': '监控器',
// Sidebar - MCP section
'nav.mcpServers': 'MCP 服务器',
'nav.manage': '管理',
@@ -4507,6 +4560,51 @@ const i18n = {
'title.issueManager': '议题管理器',
'title.issueDiscovery': '议题发现',
// Loop Monitor
'title.loopMonitor': '循环监控',
'loop.title': '循环监控',
'loop.status.created': '已创建',
'loop.status.running': '运行中',
'loop.status.paused': '已暂停',
'loop.status.completed': '已完成',
'loop.status.failed': '失败',
'loop.tabs.timeline': '时间线',
'loop.tabs.logs': '日志',
'loop.tabs.variables': '变量',
'loop.buttons.pause': '暂停',
'loop.buttons.resume': '恢复',
'loop.buttons.stop': '停止',
'loop.buttons.retry': '重试',
'loop.buttons.newLoop': '新建循环',
'loop.empty': '没有活跃的循环',
'loop.metric.iteration': '迭代',
'loop.metric.step': '步骤',
'loop.metric.duration': '耗时',
'loop.task.id': '任务',
'loop.created': '创建时间',
'loop.updated': '更新时间',
'loop.progress': '进度',
'loop.cliSequence': 'CLI 序列',
'loop.stateVariables': '状态变量',
'loop.executionHistory': '执行历史',
'loop.failureReason': '失败原因',
'loop.noLoopsFound': '未找到循环',
'loop.selectLoop': '选择一个循环查看详情',
'loop.tasks': '任务',
'loop.createTaskTitle': '创建循环任务',
'loop.loopsCount': '个循环',
'loop.paused': '循环已暂停',
'loop.resumed': '循环已恢复',
'loop.stopped': '循环已停止',
'loop.startedSuccess': '循环已启动',
'loop.taskDescription': '描述',
'loop.maxIterations': '最大迭代数',
'loop.errorPolicy': '错误策略',
'loop.pauseOnError': '错误时暂停',
'loop.retryAutomatically': '自动重试',
'loop.failImmediate': '立即失败',
'loop.successCondition': '成功条件',
// Issue Discovery
'discovery.title': '议题发现',
'discovery.description': '从多个视角发现潜在问题',

File diff suppressed because it is too large Load Diff

View File

@@ -525,6 +525,21 @@
</ul>
</div>
<!-- Loops Section -->
<div class="mb-2" id="loopsNav">
<div class="flex items-center px-4 py-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">
<i data-lucide="repeat" class="nav-section-icon mr-2"></i>
<span class="nav-section-title" data-i18n="nav.loops">Loops</span>
</div>
<ul class="space-y-0.5">
<li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="loop-monitor" data-tooltip="Loop Monitor">
<i data-lucide="activity" class="nav-icon text-cyan"></i>
<span class="nav-text flex-1" data-i18n="nav.loopMonitor">Monitor</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-cyan-light text-cyan" id="badgeLoops">0</span>
</li>
</ul>
</div>
<!-- Issues Section -->
<div class="mb-2" id="issuesNav">
<div class="flex items-center px-4 py-2 text-sm font-semibold text-muted-foreground uppercase tracking-wide">