claude-code-workflow 6.3.36 → 6.3.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/workflow/lite-execute.md +2 -0
- package/.claude/commands/workflow/lite-fix.md +108 -9
- package/.claude/skills/ccw-loop/README.md +303 -0
- package/.claude/skills/ccw-loop/SKILL.md +259 -0
- package/.claude/skills/ccw-loop/phases/actions/action-complete.md +320 -0
- package/.claude/skills/ccw-loop/phases/actions/action-debug-with-file.md +485 -0
- package/.claude/skills/ccw-loop/phases/actions/action-develop-with-file.md +365 -0
- package/.claude/skills/ccw-loop/phases/actions/action-init.md +200 -0
- package/.claude/skills/ccw-loop/phases/actions/action-menu.md +192 -0
- package/.claude/skills/ccw-loop/phases/actions/action-validate-with-file.md +307 -0
- package/.claude/skills/ccw-loop/phases/orchestrator.md +486 -0
- package/.claude/skills/ccw-loop/phases/state-schema.md +474 -0
- package/.claude/skills/ccw-loop/specs/action-catalog.md +300 -0
- package/.claude/skills/ccw-loop/specs/loop-requirements.md +192 -0
- package/.claude/skills/ccw-loop/templates/progress-template.md +175 -0
- package/.claude/skills/ccw-loop/templates/understanding-template.md +303 -0
- package/.claude/skills/ccw-loop/templates/validation-template.md +258 -0
- package/.codex/agents/action-planning-agent.md +885 -0
- package/.codex/agents/ccw-loop-executor.md +260 -0
- package/.codex/agents/cli-discuss-agent.md +391 -0
- package/.codex/agents/cli-execution-agent.md +333 -0
- package/.codex/agents/cli-explore-agent.md +186 -0
- package/.codex/agents/cli-lite-planning-agent.md +736 -0
- package/.codex/agents/cli-planning-agent.md +562 -0
- package/.codex/agents/code-developer.md +408 -0
- package/.codex/agents/conceptual-planning-agent.md +321 -0
- package/.codex/agents/context-search-agent.md +585 -0
- package/.codex/agents/debug-explore-agent.md +436 -0
- package/.codex/agents/doc-generator.md +334 -0
- package/.codex/agents/issue-plan-agent.md +417 -0
- package/.codex/agents/issue-queue-agent.md +311 -0
- package/.codex/agents/memory-bridge.md +96 -0
- package/.codex/agents/test-context-search-agent.md +402 -0
- package/.codex/agents/test-fix-agent.md +359 -0
- package/.codex/agents/ui-design-agent.md +595 -0
- package/.codex/agents/universal-executor.md +135 -0
- package/.codex/prompts/issue-discover-by-prompt.md +364 -0
- package/.codex/prompts/issue-discover.md +261 -0
- package/.codex/prompts/issue-execute.md +10 -0
- package/.codex/prompts/issue-new.md +285 -0
- package/.codex/prompts/issue-plan.md +161 -63
- package/.codex/prompts/issue-queue.md +298 -288
- package/.codex/prompts/lite-execute.md +627 -133
- package/.codex/prompts/lite-fix.md +670 -0
- package/.codex/prompts/lite-plan-a.md +337 -0
- package/.codex/prompts/lite-plan-b.md +485 -0
- package/.codex/prompts/{lite-plan.md → lite-plan-c.md} +601 -469
- package/.codex/skills/ccw-loop/README.md +171 -0
- package/.codex/skills/ccw-loop/SKILL.md +349 -0
- package/.codex/skills/ccw-loop/phases/actions/action-complete.md +269 -0
- package/.codex/skills/ccw-loop/phases/actions/action-debug.md +286 -0
- package/.codex/skills/ccw-loop/phases/actions/action-develop.md +183 -0
- package/.codex/skills/ccw-loop/phases/actions/action-init.md +164 -0
- package/.codex/skills/ccw-loop/phases/actions/action-menu.md +205 -0
- package/.codex/skills/ccw-loop/phases/actions/action-validate.md +250 -0
- package/.codex/skills/ccw-loop/phases/orchestrator.md +416 -0
- package/.codex/skills/ccw-loop/phases/state-schema.md +388 -0
- package/.codex/skills/ccw-loop/specs/action-catalog.md +182 -0
- package/.codex/skills/ccw-loop-b/README.md +102 -0
- package/.codex/skills/ccw-loop-b/SKILL.md +322 -0
- package/.codex/skills/ccw-loop-b/phases/orchestrator.md +257 -0
- package/.codex/skills/ccw-loop-b/phases/state-schema.md +181 -0
- package/ccw/dist/cli.d.ts.map +1 -1
- package/ccw/dist/cli.js +12 -1
- package/ccw/dist/cli.js.map +1 -1
- package/ccw/dist/commands/cli.d.ts.map +1 -1
- package/ccw/dist/commands/cli.js +14 -1
- package/ccw/dist/commands/cli.js.map +1 -1
- package/ccw/dist/commands/install.d.ts.map +1 -1
- package/ccw/dist/commands/install.js +38 -7
- package/ccw/dist/commands/install.js.map +1 -1
- package/ccw/dist/commands/issue.d.ts +3 -0
- package/ccw/dist/commands/issue.d.ts.map +1 -1
- package/ccw/dist/commands/issue.js +107 -0
- package/ccw/dist/commands/issue.js.map +1 -1
- package/ccw/dist/commands/loop.d.ts +10 -0
- package/ccw/dist/commands/loop.d.ts.map +1 -0
- package/ccw/dist/commands/loop.js +289 -0
- package/ccw/dist/commands/loop.js.map +1 -0
- package/ccw/dist/commands/upgrade.js +1 -1
- package/ccw/dist/commands/upgrade.js.map +1 -1
- package/ccw/dist/config/litellm-api-config-manager.d.ts.map +1 -1
- package/ccw/dist/config/litellm-api-config-manager.js +3 -2
- package/ccw/dist/config/litellm-api-config-manager.js.map +1 -1
- package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
- package/ccw/dist/core/dashboard-generator.js +4 -1
- package/ccw/dist/core/dashboard-generator.js.map +1 -1
- package/ccw/dist/core/memory-embedder-bridge.d.ts.map +1 -1
- package/ccw/dist/core/memory-embedder-bridge.js +2 -5
- package/ccw/dist/core/memory-embedder-bridge.js.map +1 -1
- package/ccw/dist/core/routes/claude-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/claude-routes.js +5 -3
- package/ccw/dist/core/routes/claude-routes.js.map +1 -1
- package/ccw/dist/core/routes/cli-routes.d.ts +6 -0
- package/ccw/dist/core/routes/cli-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/cli-routes.js +42 -13
- package/ccw/dist/core/routes/cli-routes.js.map +1 -1
- package/ccw/dist/core/routes/cli-settings-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/cli-settings-routes.js +44 -0
- package/ccw/dist/core/routes/cli-settings-routes.js.map +1 -1
- package/ccw/dist/core/routes/codexlens/config-handlers.d.ts.map +1 -1
- package/ccw/dist/core/routes/codexlens/config-handlers.js +7 -6
- package/ccw/dist/core/routes/codexlens/config-handlers.js.map +1 -1
- package/ccw/dist/core/routes/codexlens/semantic-handlers.d.ts.map +1 -1
- package/ccw/dist/core/routes/codexlens/semantic-handlers.js +5 -4
- package/ccw/dist/core/routes/codexlens/semantic-handlers.js.map +1 -1
- package/ccw/dist/core/routes/core-memory-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/core-memory-routes.js +4 -2
- package/ccw/dist/core/routes/core-memory-routes.js.map +1 -1
- package/ccw/dist/core/routes/files-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/files-routes.js +4 -2
- package/ccw/dist/core/routes/files-routes.js.map +1 -1
- package/ccw/dist/core/routes/graph-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/graph-routes.js +17 -2
- package/ccw/dist/core/routes/graph-routes.js.map +1 -1
- package/ccw/dist/core/routes/issue-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/issue-routes.js +280 -33
- package/ccw/dist/core/routes/issue-routes.js.map +1 -1
- package/ccw/dist/core/routes/loop-routes.d.ts +24 -0
- package/ccw/dist/core/routes/loop-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/loop-routes.js +334 -0
- package/ccw/dist/core/routes/loop-routes.js.map +1 -0
- package/ccw/dist/core/routes/loop-v2-routes.d.ts +44 -0
- package/ccw/dist/core/routes/loop-v2-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/loop-v2-routes.js +1260 -0
- package/ccw/dist/core/routes/loop-v2-routes.js.map +1 -0
- package/ccw/dist/core/routes/memory-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/memory-routes.js +2 -1
- package/ccw/dist/core/routes/memory-routes.js.map +1 -1
- package/ccw/dist/core/routes/system-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/system-routes.js +3 -2
- package/ccw/dist/core/routes/system-routes.js.map +1 -1
- package/ccw/dist/core/routes/task-routes.d.ts +12 -0
- package/ccw/dist/core/routes/task-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/task-routes.js +321 -0
- package/ccw/dist/core/routes/task-routes.js.map +1 -0
- package/ccw/dist/core/routes/test-loop-routes.d.ts +11 -0
- package/ccw/dist/core/routes/test-loop-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/test-loop-routes.js +298 -0
- package/ccw/dist/core/routes/test-loop-routes.js.map +1 -0
- package/ccw/dist/core/server.d.ts.map +1 -1
- package/ccw/dist/core/server.js +47 -5
- package/ccw/dist/core/server.js.map +1 -1
- package/ccw/dist/core/websocket.d.ts +59 -0
- package/ccw/dist/core/websocket.d.ts.map +1 -1
- package/ccw/dist/core/websocket.js +34 -0
- package/ccw/dist/core/websocket.js.map +1 -1
- package/ccw/dist/tools/claude-cli-tools.d.ts +40 -0
- package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
- package/ccw/dist/tools/claude-cli-tools.js +119 -0
- package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
- package/ccw/dist/tools/codex-lens-lsp.d.ts.map +1 -1
- package/ccw/dist/tools/codex-lens-lsp.js +2 -5
- package/ccw/dist/tools/codex-lens-lsp.js.map +1 -1
- package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
- package/ccw/dist/tools/codex-lens.js +22 -32
- package/ccw/dist/tools/codex-lens.js.map +1 -1
- package/ccw/dist/tools/litellm-client.d.ts +6 -0
- package/ccw/dist/tools/litellm-client.d.ts.map +1 -1
- package/ccw/dist/tools/litellm-client.js +15 -2
- package/ccw/dist/tools/litellm-client.js.map +1 -1
- package/ccw/dist/tools/loop-manager.d.ts +84 -0
- package/ccw/dist/tools/loop-manager.d.ts.map +1 -0
- package/ccw/dist/tools/loop-manager.js +425 -0
- package/ccw/dist/tools/loop-manager.js.map +1 -0
- package/ccw/dist/tools/loop-state-manager.d.ts +47 -0
- package/ccw/dist/tools/loop-state-manager.d.ts.map +1 -0
- package/ccw/dist/tools/loop-state-manager.js +149 -0
- package/ccw/dist/tools/loop-state-manager.js.map +1 -0
- package/ccw/dist/tools/loop-task-manager.d.ts +149 -0
- package/ccw/dist/tools/loop-task-manager.d.ts.map +1 -0
- package/ccw/dist/tools/loop-task-manager.js +270 -0
- package/ccw/dist/tools/loop-task-manager.js.map +1 -0
- package/ccw/dist/tools/native-session-discovery.d.ts.map +1 -1
- package/ccw/dist/tools/native-session-discovery.js +35 -7
- package/ccw/dist/tools/native-session-discovery.js.map +1 -1
- package/ccw/dist/types/index.d.ts +1 -0
- package/ccw/dist/types/index.d.ts.map +1 -1
- package/ccw/dist/types/index.js +1 -0
- package/ccw/dist/types/index.js.map +1 -1
- package/ccw/dist/types/loop.d.ts +257 -0
- package/ccw/dist/types/loop.d.ts.map +1 -0
- package/ccw/dist/types/loop.js +17 -0
- package/ccw/dist/types/loop.js.map +1 -0
- package/ccw/dist/utils/codexlens-path.d.ts +36 -0
- package/ccw/dist/utils/codexlens-path.d.ts.map +1 -0
- package/ccw/dist/utils/codexlens-path.js +56 -0
- package/ccw/dist/utils/codexlens-path.js.map +1 -0
- package/ccw/dist/utils/uv-manager.d.ts.map +1 -1
- package/ccw/dist/utils/uv-manager.js +3 -2
- package/ccw/dist/utils/uv-manager.js.map +1 -1
- package/ccw/src/cli.ts +13 -1
- package/ccw/src/commands/cli.ts +14 -1
- package/ccw/src/commands/install.ts +50 -7
- package/ccw/src/commands/issue.ts +119 -0
- package/ccw/src/commands/loop.ts +344 -0
- package/ccw/src/commands/upgrade.ts +1 -1
- package/ccw/src/config/litellm-api-config-manager.ts +3 -2
- package/ccw/src/core/dashboard-generator.ts +4 -1
- package/ccw/src/core/memory-embedder-bridge.ts +2 -6
- package/ccw/src/core/routes/claude-routes.ts +5 -3
- package/ccw/src/core/routes/cli-routes.ts +48 -16
- package/ccw/src/core/routes/cli-settings-routes.ts +47 -0
- package/ccw/src/core/routes/codexlens/config-handlers.ts +7 -6
- package/ccw/src/core/routes/codexlens/semantic-handlers.ts +5 -4
- package/ccw/src/core/routes/core-memory-routes.ts +4 -2
- package/ccw/src/core/routes/files-routes.ts +4 -2
- package/ccw/src/core/routes/graph-routes.ts +18 -2
- package/ccw/src/core/routes/issue-routes.ts +308 -33
- package/ccw/src/core/routes/loop-routes.ts +386 -0
- package/ccw/src/core/routes/loop-v2-routes.ts +1470 -0
- package/ccw/src/core/routes/memory-routes.ts +2 -1
- package/ccw/src/core/routes/system-routes.ts +3 -2
- package/ccw/src/core/routes/task-routes.ts +361 -0
- package/ccw/src/core/routes/test-loop-routes.ts +312 -0
- package/ccw/src/core/server.ts +49 -5
- package/ccw/src/core/websocket.ts +104 -0
- package/ccw/src/templates/dashboard-css/02-session.css +2 -0
- package/ccw/src/templates/dashboard-css/04-lite-tasks.css +103 -1
- package/ccw/src/templates/dashboard-css/12-cli-legacy.css +56 -0
- package/ccw/src/templates/dashboard-css/32-issue-manager.css +32 -0
- package/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +55 -0
- package/ccw/src/templates/dashboard-css/36-loop-monitor.css +1896 -0
- package/ccw/src/templates/dashboard-css/36-loop-monitor.css.backup +1877 -0
- package/ccw/src/templates/dashboard-js/components/cli-history.js +48 -48
- package/ccw/src/templates/dashboard-js/components/cli-status.js +64 -3
- package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +251 -110
- package/ccw/src/templates/dashboard-js/components/navigation.js +16 -0
- package/ccw/src/templates/dashboard-js/components/notifications.js +22 -0
- package/ccw/src/templates/dashboard-js/components/version-check.js +38 -0
- package/ccw/src/templates/dashboard-js/i18n.js +601 -1
- package/ccw/src/templates/dashboard-js/state.js +2 -0
- package/ccw/src/templates/dashboard-js/views/cli-manager.js +4 -3
- package/ccw/src/templates/dashboard-js/views/issue-manager.js +183 -1
- package/ccw/src/templates/dashboard-js/views/lite-tasks.js +55 -11
- package/ccw/src/templates/dashboard-js/views/loop-monitor.js +3345 -0
- package/ccw/src/templates/dashboard.html +68 -4
- package/ccw/src/tools/claude-cli-tools.ts +143 -0
- package/ccw/src/tools/codex-lens-lsp.ts +2 -5
- package/ccw/src/tools/codex-lens.ts +27 -38
- package/ccw/src/tools/litellm-client.ts +16 -2
- package/ccw/src/tools/loop-manager.ts +519 -0
- package/ccw/src/tools/loop-state-manager.ts +173 -0
- package/ccw/src/tools/loop-task-manager.ts +391 -0
- package/ccw/src/tools/native-session-discovery.ts +38 -7
- package/ccw/src/types/index.ts +1 -0
- package/ccw/src/types/loop.ts +316 -0
- package/ccw/src/utils/codexlens-path.ts +60 -0
- package/ccw/src/utils/uv-manager.ts +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,1260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop V2 Routes Module
|
|
3
|
+
* CCW Loop System - Simplified HTTP API endpoints for Dashboard
|
|
4
|
+
* Provides simplified loop CRUD operations independent of task files
|
|
5
|
+
*
|
|
6
|
+
* Loop Endpoints:
|
|
7
|
+
* - GET /api/loops/v2 - List all loops with pagination
|
|
8
|
+
* - POST /api/loops/v2 - Create loop with {title, description, max_iterations}
|
|
9
|
+
* - GET /api/loops/v2/:loopId - Get loop details
|
|
10
|
+
* - PUT /api/loops/v2/:loopId - Update loop metadata (title, description, max_iterations, tags, priority, notes)
|
|
11
|
+
* - PATCH /api/loops/v2/:loopId/status - Quick status update with {status}
|
|
12
|
+
* - DELETE /api/loops/v2/:loopId - Delete loop
|
|
13
|
+
* - POST /api/loops/v2/:loopId/start - Start loop execution
|
|
14
|
+
* - POST /api/loops/v2/:loopId/pause - Pause loop
|
|
15
|
+
* - POST /api/loops/v2/:loopId/resume - Resume loop
|
|
16
|
+
* - POST /api/loops/v2/:loopId/stop - Stop loop
|
|
17
|
+
*
|
|
18
|
+
* Task Management Endpoints:
|
|
19
|
+
* - POST /api/loops/v2/:loopId/tasks - Add task to loop
|
|
20
|
+
* - GET /api/loops/v2/:loopId/tasks - List all tasks for loop
|
|
21
|
+
* - PUT /api/loops/v2/tasks/:taskId - Update task (requires loop_id in body)
|
|
22
|
+
* - DELETE /api/loops/v2/tasks/:taskId - Delete task (requires loop_id query param)
|
|
23
|
+
* - PUT /api/loops/v2/:loopId/tasks/reorder - Reorder tasks with {ordered_task_ids: string[]}
|
|
24
|
+
*
|
|
25
|
+
* Advanced Task Features:
|
|
26
|
+
* - POST /api/loops/v2/:loopId/import - Import tasks from issue with {issue_id}
|
|
27
|
+
* - POST /api/loops/v2/:loopId/generate - Generate tasks via Gemini with {tool?, count?}
|
|
28
|
+
*/
|
|
29
|
+
import { join } from 'path';
|
|
30
|
+
import { randomBytes } from 'crypto';
|
|
31
|
+
import * as os from 'os';
|
|
32
|
+
import { LoopStatus } from '../../types/loop.js';
|
|
33
|
+
import { TaskStorageManager } from '../../tools/loop-task-manager.js';
|
|
34
|
+
import { executeCliTool } from '../../tools/cli-executor.js';
|
|
35
|
+
import { loadClaudeCliTools } from '../../tools/claude-cli-tools.js';
|
|
36
|
+
/**
|
|
37
|
+
* Module-level cache for CLI tools configuration
|
|
38
|
+
* Loaded once at server startup to avoid repeated file I/O
|
|
39
|
+
*/
|
|
40
|
+
let cachedEnabledTools = null;
|
|
41
|
+
/**
|
|
42
|
+
* Initialize CLI tools cache at server startup
|
|
43
|
+
* Should be called once when the server starts
|
|
44
|
+
*/
|
|
45
|
+
export function initializeCliToolsCache() {
|
|
46
|
+
try {
|
|
47
|
+
const cliToolsConfig = loadClaudeCliTools(os.homedir());
|
|
48
|
+
const enabledTools = Object.entries(cliToolsConfig.tools || {})
|
|
49
|
+
.filter(([_, config]) => config.enabled === true)
|
|
50
|
+
.map(([name]) => name);
|
|
51
|
+
cachedEnabledTools = ['bash', ...enabledTools];
|
|
52
|
+
console.log('[Loop V2] CLI tools cache initialized:', cachedEnabledTools);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.error('[Loop V2] Failed to initialize CLI tools cache:', err);
|
|
56
|
+
// Fallback to basic tools if config loading fails
|
|
57
|
+
cachedEnabledTools = ['bash', 'gemini', 'qwen', 'codex', 'claude'];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Clear CLI tools cache (for testing or config reload)
|
|
62
|
+
*/
|
|
63
|
+
export function clearCliToolsCache() {
|
|
64
|
+
cachedEnabledTools = null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Handle V2 loop routes
|
|
68
|
+
* @returns true if route was handled, false otherwise
|
|
69
|
+
*/
|
|
70
|
+
export async function handleLoopV2Routes(ctx) {
|
|
71
|
+
const { pathname, req, res, initialPath, handlePostRequest, url, broadcastToClients } = ctx;
|
|
72
|
+
// Get workflow directory from initialPath
|
|
73
|
+
const workflowDir = initialPath || process.cwd();
|
|
74
|
+
const loopDir = join(workflowDir, '.workflow', '.loop');
|
|
75
|
+
// Helper to broadcast loop state updates
|
|
76
|
+
const broadcastStateUpdate = (loopId, status) => {
|
|
77
|
+
try {
|
|
78
|
+
broadcastToClients({
|
|
79
|
+
type: 'LOOP_STATE_UPDATE',
|
|
80
|
+
loop_id: loopId,
|
|
81
|
+
status: status,
|
|
82
|
+
updated_at: new Date().toISOString()
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// Silently ignore broadcast errors
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
// Helper to generate loop ID
|
|
90
|
+
const generateLoopId = () => {
|
|
91
|
+
const timestamp = new Date().toISOString().replace(/[-:]/g, '').split('.')[0];
|
|
92
|
+
const random = randomBytes(4).toString('hex');
|
|
93
|
+
return `loop-v2-${timestamp}-${random}`;
|
|
94
|
+
};
|
|
95
|
+
// Helper to read loop storage
|
|
96
|
+
const readLoopStorage = async (loopId) => {
|
|
97
|
+
const { readFile } = await import('fs/promises');
|
|
98
|
+
const { existsSync } = await import('fs');
|
|
99
|
+
const filePath = join(loopDir, `${loopId}.json`);
|
|
100
|
+
if (!existsSync(filePath)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const content = await readFile(filePath, 'utf-8');
|
|
105
|
+
return JSON.parse(content);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
// Helper to write loop storage
|
|
112
|
+
const writeLoopStorage = async (loop) => {
|
|
113
|
+
const { writeFile, mkdir } = await import('fs/promises');
|
|
114
|
+
const { existsSync } = await import('fs');
|
|
115
|
+
if (!existsSync(loopDir)) {
|
|
116
|
+
await mkdir(loopDir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
const filePath = join(loopDir, `${loop.loop_id}.json`);
|
|
119
|
+
await writeFile(filePath, JSON.stringify(loop, null, 2), 'utf-8');
|
|
120
|
+
};
|
|
121
|
+
// Helper to delete loop storage
|
|
122
|
+
const deleteLoopStorage = async (loopId) => {
|
|
123
|
+
const { unlink } = await import('fs/promises');
|
|
124
|
+
const { existsSync } = await import('fs');
|
|
125
|
+
const filePath = join(loopDir, `${loopId}.json`);
|
|
126
|
+
if (existsSync(filePath)) {
|
|
127
|
+
await unlink(filePath);
|
|
128
|
+
}
|
|
129
|
+
// Also delete tasks.jsonl if exists
|
|
130
|
+
const tasksPath = join(loopDir, `${loopId}.tasks.jsonl`);
|
|
131
|
+
if (existsSync(tasksPath)) {
|
|
132
|
+
await unlink(tasksPath).catch(() => { });
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
// Helper to list all loops
|
|
136
|
+
const listLoops = async () => {
|
|
137
|
+
const { readdir } = await import('fs/promises');
|
|
138
|
+
const { existsSync } = await import('fs');
|
|
139
|
+
if (!existsSync(loopDir)) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
const files = await readdir(loopDir);
|
|
143
|
+
const loopFiles = files.filter(f => f.startsWith('loop-v2-') && f.endsWith('.json'));
|
|
144
|
+
const loops = [];
|
|
145
|
+
for (const file of loopFiles) {
|
|
146
|
+
const loopId = file.replace('.json', '');
|
|
147
|
+
const loop = await readLoopStorage(loopId);
|
|
148
|
+
if (loop) {
|
|
149
|
+
loops.push(loop);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return loops;
|
|
153
|
+
};
|
|
154
|
+
// ==== EXACT PATH ROUTES ====
|
|
155
|
+
// POST /api/loops/v2 - Create loop with simplified fields
|
|
156
|
+
if (pathname === '/api/loops/v2' && req.method === 'POST') {
|
|
157
|
+
handlePostRequest(req, res, async (body) => {
|
|
158
|
+
const { title, description, max_iterations } = body;
|
|
159
|
+
// Validation
|
|
160
|
+
if (!title || typeof title !== 'string' || title.trim().length === 0) {
|
|
161
|
+
return { success: false, error: 'title is required and must be non-empty', status: 400 };
|
|
162
|
+
}
|
|
163
|
+
if (description !== undefined && typeof description !== 'string') {
|
|
164
|
+
return { success: false, error: 'description must be a string', status: 400 };
|
|
165
|
+
}
|
|
166
|
+
if (max_iterations !== undefined && (typeof max_iterations !== 'number' || max_iterations < 1)) {
|
|
167
|
+
return { success: false, error: 'max_iterations must be a positive number', status: 400 };
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const loopId = generateLoopId();
|
|
171
|
+
const now = new Date().toISOString();
|
|
172
|
+
const loop = {
|
|
173
|
+
loop_id: loopId,
|
|
174
|
+
title: title.trim(),
|
|
175
|
+
description: description?.trim() || '',
|
|
176
|
+
max_iterations: max_iterations || 10,
|
|
177
|
+
status: LoopStatus.CREATED,
|
|
178
|
+
current_iteration: 0,
|
|
179
|
+
created_at: now,
|
|
180
|
+
updated_at: now
|
|
181
|
+
};
|
|
182
|
+
await writeLoopStorage(loop);
|
|
183
|
+
// Broadcast creation
|
|
184
|
+
broadcastStateUpdate(loopId, LoopStatus.CREATED);
|
|
185
|
+
return { success: true, data: loop };
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
return { success: false, error: error.message, status: 500 };
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
// GET /api/loops/v2 - List all loops with pagination
|
|
194
|
+
if (pathname === '/api/loops/v2' && req.method === 'GET') {
|
|
195
|
+
try {
|
|
196
|
+
const loops = await listLoops();
|
|
197
|
+
// Parse query params for pagination and filtering
|
|
198
|
+
const searchParams = url?.searchParams;
|
|
199
|
+
let filteredLoops = loops;
|
|
200
|
+
// Filter by status
|
|
201
|
+
const statusFilter = searchParams?.get('status');
|
|
202
|
+
if (statusFilter && statusFilter !== 'all') {
|
|
203
|
+
filteredLoops = filteredLoops.filter(l => l.status === statusFilter);
|
|
204
|
+
}
|
|
205
|
+
// Sort by updated_at (most recent first)
|
|
206
|
+
filteredLoops.sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime());
|
|
207
|
+
// Parse pagination params
|
|
208
|
+
const limit = parseInt(searchParams?.get('limit') || '50', 10);
|
|
209
|
+
const offset = parseInt(searchParams?.get('offset') || '0', 10);
|
|
210
|
+
// Apply pagination
|
|
211
|
+
const paginatedLoops = filteredLoops.slice(offset, offset + limit);
|
|
212
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
213
|
+
res.end(JSON.stringify({
|
|
214
|
+
success: true,
|
|
215
|
+
data: paginatedLoops,
|
|
216
|
+
total: filteredLoops.length,
|
|
217
|
+
limit,
|
|
218
|
+
offset,
|
|
219
|
+
hasMore: offset + limit < filteredLoops.length,
|
|
220
|
+
timestamp: new Date().toISOString()
|
|
221
|
+
}));
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
226
|
+
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// ==== NESTED PATH ROUTES (more specific patterns first) ====
|
|
231
|
+
// POST /api/loops/v2/:loopId/start - Start loop execution
|
|
232
|
+
if (pathname.match(/\/api\/loops\/v2\/[^/]+\/start$/) && req.method === 'POST') {
|
|
233
|
+
const loopId = pathname.split('/').slice(-2)[0];
|
|
234
|
+
if (!loopId || !isValidId(loopId)) {
|
|
235
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
236
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' }));
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const loop = await readLoopStorage(loopId);
|
|
241
|
+
if (!loop) {
|
|
242
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
243
|
+
res.end(JSON.stringify({ success: false, error: 'Loop not found' }));
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
// Can only start created or paused loops
|
|
247
|
+
if (!['created', 'paused'].includes(loop.status.toLowerCase())) {
|
|
248
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
249
|
+
res.end(JSON.stringify({
|
|
250
|
+
success: false,
|
|
251
|
+
error: `Cannot start loop with status: ${loop.status}`
|
|
252
|
+
}));
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
// Update loop status
|
|
256
|
+
loop.status = LoopStatus.RUNNING;
|
|
257
|
+
loop.updated_at = new Date().toISOString();
|
|
258
|
+
await writeLoopStorage(loop);
|
|
259
|
+
// Broadcast state update
|
|
260
|
+
broadcastStateUpdate(loopId, LoopStatus.RUNNING);
|
|
261
|
+
// Trigger ccw-loop skill execution (non-blocking)
|
|
262
|
+
// The skill will check status before each action and exit gracefully on pause/stop
|
|
263
|
+
executeCliTool({
|
|
264
|
+
tool: 'claude',
|
|
265
|
+
prompt: `/ccw-loop --loop-id ${loopId} --auto`,
|
|
266
|
+
mode: 'write',
|
|
267
|
+
workingDir: workflowDir
|
|
268
|
+
}).catch((error) => {
|
|
269
|
+
// Log error but don't fail the start request
|
|
270
|
+
console.error(`Failed to trigger ccw-loop skill for ${loopId}:`, error);
|
|
271
|
+
// Update loop status to failed
|
|
272
|
+
readLoopStorage(loopId).then(async (failedLoop) => {
|
|
273
|
+
if (failedLoop) {
|
|
274
|
+
failedLoop.status = LoopStatus.FAILED;
|
|
275
|
+
failedLoop.failure_reason = `Skill execution failed: ${error.message}`;
|
|
276
|
+
failedLoop.completed_at = new Date().toISOString();
|
|
277
|
+
await writeLoopStorage(failedLoop);
|
|
278
|
+
broadcastStateUpdate(loopId, LoopStatus.FAILED);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
283
|
+
res.end(JSON.stringify({ success: true, data: loop, message: 'Loop started' }));
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
288
|
+
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// POST /api/loops/v2/:loopId/pause - Pause loop
|
|
293
|
+
if (pathname.match(/\/api\/loops\/v2\/[^/]+\/pause$/) && req.method === 'POST') {
|
|
294
|
+
const loopId = pathname.split('/').slice(-2)[0];
|
|
295
|
+
if (!loopId || !isValidId(loopId)) {
|
|
296
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
297
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' }));
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
const loop = await readLoopStorage(loopId);
|
|
302
|
+
if (!loop) {
|
|
303
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
304
|
+
res.end(JSON.stringify({ success: false, error: 'Loop not found' }));
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
// Can only pause running loops
|
|
308
|
+
if (loop.status !== LoopStatus.RUNNING) {
|
|
309
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
310
|
+
res.end(JSON.stringify({
|
|
311
|
+
success: false,
|
|
312
|
+
error: `Cannot pause loop with status: ${loop.status}`
|
|
313
|
+
}));
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
loop.status = LoopStatus.PAUSED;
|
|
317
|
+
loop.updated_at = new Date().toISOString();
|
|
318
|
+
await writeLoopStorage(loop);
|
|
319
|
+
broadcastStateUpdate(loopId, LoopStatus.PAUSED);
|
|
320
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
321
|
+
res.end(JSON.stringify({ success: true, data: loop, message: 'Loop paused' }));
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
326
|
+
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// POST /api/loops/v2/:loopId/resume - Resume loop
|
|
331
|
+
if (pathname.match(/\/api\/loops\/v2\/[^/]+\/resume$/) && req.method === 'POST') {
|
|
332
|
+
const loopId = pathname.split('/').slice(-2)[0];
|
|
333
|
+
if (!loopId || !isValidId(loopId)) {
|
|
334
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
335
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' }));
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const loop = await readLoopStorage(loopId);
|
|
340
|
+
if (!loop) {
|
|
341
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
342
|
+
res.end(JSON.stringify({ success: false, error: 'Loop not found' }));
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
// Can only resume paused loops
|
|
346
|
+
if (loop.status !== LoopStatus.PAUSED) {
|
|
347
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
348
|
+
res.end(JSON.stringify({
|
|
349
|
+
success: false,
|
|
350
|
+
error: `Cannot resume loop with status: ${loop.status}`
|
|
351
|
+
}));
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
loop.status = LoopStatus.RUNNING;
|
|
355
|
+
loop.updated_at = new Date().toISOString();
|
|
356
|
+
await writeLoopStorage(loop);
|
|
357
|
+
broadcastStateUpdate(loopId, LoopStatus.RUNNING);
|
|
358
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
359
|
+
res.end(JSON.stringify({ success: true, data: loop, message: 'Loop resumed' }));
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
364
|
+
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// POST /api/loops/v2/:loopId/stop - Stop loop
|
|
369
|
+
if (pathname.match(/\/api\/loops\/v2\/[^/]+\/stop$/) && req.method === 'POST') {
|
|
370
|
+
const loopId = pathname.split('/').slice(-2)[0];
|
|
371
|
+
if (!loopId || !isValidId(loopId)) {
|
|
372
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
373
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' }));
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
const loop = await readLoopStorage(loopId);
|
|
378
|
+
if (!loop) {
|
|
379
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
380
|
+
res.end(JSON.stringify({ success: false, error: 'Loop not found' }));
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
// Can only stop running or paused loops
|
|
384
|
+
if (![LoopStatus.RUNNING, LoopStatus.PAUSED, LoopStatus.CREATED].includes(loop.status)) {
|
|
385
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
386
|
+
res.end(JSON.stringify({
|
|
387
|
+
success: false,
|
|
388
|
+
error: `Cannot stop loop with status: ${loop.status}`
|
|
389
|
+
}));
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
loop.status = LoopStatus.FAILED;
|
|
393
|
+
loop.failure_reason = 'Manually stopped by user';
|
|
394
|
+
loop.completed_at = new Date().toISOString();
|
|
395
|
+
loop.updated_at = loop.completed_at;
|
|
396
|
+
await writeLoopStorage(loop);
|
|
397
|
+
broadcastStateUpdate(loopId, LoopStatus.FAILED);
|
|
398
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
399
|
+
res.end(JSON.stringify({ success: true, data: loop, message: 'Loop stopped' }));
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
404
|
+
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// ==== SINGLE PARAM ROUTES (must come after nested routes) ====
|
|
409
|
+
// GET /api/loops/v2/:loopId - Get loop details
|
|
410
|
+
if (pathname.match(/^\/api\/loops\/v2\/[^/]+$/) && req.method === 'GET') {
|
|
411
|
+
const loopId = pathname.split('/').pop();
|
|
412
|
+
if (!loopId || !isValidId(loopId)) {
|
|
413
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
414
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' }));
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
const loop = await readLoopStorage(loopId);
|
|
419
|
+
if (!loop) {
|
|
420
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
421
|
+
res.end(JSON.stringify({ success: false, error: 'Loop not found' }));
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
425
|
+
res.end(JSON.stringify({ success: true, data: loop }));
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
430
|
+
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// PUT /api/loops/v2/:loopId - Update loop metadata
|
|
435
|
+
if (pathname.match(/^\/api\/loops\/v2\/[^/]+$/) && req.method === 'PUT') {
|
|
436
|
+
const loopId = pathname.split('/').pop();
|
|
437
|
+
if (!loopId || !isValidId(loopId)) {
|
|
438
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
439
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' }));
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
handlePostRequest(req, res, async (body) => {
|
|
443
|
+
const { title, description, max_iterations, tags, priority, notes } = body;
|
|
444
|
+
try {
|
|
445
|
+
const loop = await readLoopStorage(loopId);
|
|
446
|
+
if (!loop) {
|
|
447
|
+
return { success: false, error: 'Loop not found', status: 404 };
|
|
448
|
+
}
|
|
449
|
+
// Can only update created or paused loops
|
|
450
|
+
if (![LoopStatus.CREATED, LoopStatus.PAUSED, LoopStatus.FAILED, LoopStatus.COMPLETED].includes(loop.status)) {
|
|
451
|
+
return { success: false, error: `Cannot update loop with status: ${loop.status}`, status: 400 };
|
|
452
|
+
}
|
|
453
|
+
// Validate and apply updates
|
|
454
|
+
if (title !== undefined) {
|
|
455
|
+
if (typeof title !== 'string' || title.trim().length === 0) {
|
|
456
|
+
return { success: false, error: 'title must be a non-empty string', status: 400 };
|
|
457
|
+
}
|
|
458
|
+
loop.title = title.trim();
|
|
459
|
+
}
|
|
460
|
+
if (description !== undefined) {
|
|
461
|
+
if (typeof description !== 'string') {
|
|
462
|
+
return { success: false, error: 'description must be a string', status: 400 };
|
|
463
|
+
}
|
|
464
|
+
loop.description = description.trim();
|
|
465
|
+
}
|
|
466
|
+
if (max_iterations !== undefined) {
|
|
467
|
+
if (typeof max_iterations !== 'number' || max_iterations < 1) {
|
|
468
|
+
return { success: false, error: 'max_iterations must be a positive number', status: 400 };
|
|
469
|
+
}
|
|
470
|
+
loop.max_iterations = max_iterations;
|
|
471
|
+
}
|
|
472
|
+
// Extended metadata fields
|
|
473
|
+
if (tags !== undefined) {
|
|
474
|
+
if (!Array.isArray(tags) || !tags.every(t => typeof t === 'string')) {
|
|
475
|
+
return { success: false, error: 'tags must be an array of strings', status: 400 };
|
|
476
|
+
}
|
|
477
|
+
loop.tags = tags;
|
|
478
|
+
}
|
|
479
|
+
if (priority !== undefined) {
|
|
480
|
+
if (!['low', 'medium', 'high'].includes(priority)) {
|
|
481
|
+
return { success: false, error: 'priority must be one of: low, medium, high', status: 400 };
|
|
482
|
+
}
|
|
483
|
+
loop.priority = priority;
|
|
484
|
+
}
|
|
485
|
+
if (notes !== undefined) {
|
|
486
|
+
if (typeof notes !== 'string') {
|
|
487
|
+
return { success: false, error: 'notes must be a string', status: 400 };
|
|
488
|
+
}
|
|
489
|
+
loop.notes = notes.trim();
|
|
490
|
+
}
|
|
491
|
+
loop.updated_at = new Date().toISOString();
|
|
492
|
+
await writeLoopStorage(loop);
|
|
493
|
+
broadcastStateUpdate(loopId, loop.status);
|
|
494
|
+
return { success: true, data: loop };
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
return { success: false, error: error.message, status: 500 };
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
// PATCH /api/loops/v2/:loopId/status - Quick status update
|
|
503
|
+
if (pathname.match(/\/api\/loops\/v2\/[^/]+\/status$/) && req.method === 'PATCH') {
|
|
504
|
+
const loopId = pathname.split('/').slice(-2)[0];
|
|
505
|
+
if (!loopId || !isValidId(loopId)) {
|
|
506
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
507
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' }));
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
handlePostRequest(req, res, async (body) => {
|
|
511
|
+
const { status } = body;
|
|
512
|
+
if (!status || typeof status !== 'string') {
|
|
513
|
+
return { success: false, error: 'status is required', status: 400 };
|
|
514
|
+
}
|
|
515
|
+
if (!Object.values(LoopStatus).includes(status)) {
|
|
516
|
+
return { success: false, error: `Invalid status: ${status}`, status: 400 };
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
const loop = await readLoopStorage(loopId);
|
|
520
|
+
if (!loop) {
|
|
521
|
+
return { success: false, error: 'Loop not found', status: 404 };
|
|
522
|
+
}
|
|
523
|
+
loop.status = status;
|
|
524
|
+
loop.updated_at = new Date().toISOString();
|
|
525
|
+
if (status === LoopStatus.COMPLETED && !loop.completed_at) {
|
|
526
|
+
loop.completed_at = new Date().toISOString();
|
|
527
|
+
}
|
|
528
|
+
await writeLoopStorage(loop);
|
|
529
|
+
broadcastStateUpdate(loopId, loop.status);
|
|
530
|
+
return { success: true, data: loop };
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
return { success: false, error: error.message, status: 500 };
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
// DELETE /api/loops/v2/:loopId - Delete loop
|
|
539
|
+
if (pathname.match(/^\/api\/loops\/v2\/[^/]+$/) && req.method === 'DELETE') {
|
|
540
|
+
const loopId = pathname.split('/').pop();
|
|
541
|
+
if (!loopId || !isValidId(loopId)) {
|
|
542
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
543
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' }));
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
const loop = await readLoopStorage(loopId);
|
|
548
|
+
if (!loop) {
|
|
549
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
550
|
+
res.end(JSON.stringify({ success: false, error: 'Loop not found' }));
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
// Cannot delete running loops
|
|
554
|
+
if (loop.status === LoopStatus.RUNNING) {
|
|
555
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
556
|
+
res.end(JSON.stringify({
|
|
557
|
+
success: false,
|
|
558
|
+
error: 'Cannot delete running loop. Stop it first.'
|
|
559
|
+
}));
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
await deleteLoopStorage(loopId);
|
|
563
|
+
// Broadcast deletion
|
|
564
|
+
try {
|
|
565
|
+
broadcastToClients({
|
|
566
|
+
type: 'LOOP_DELETED',
|
|
567
|
+
loop_id: loopId
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
// Ignore broadcast errors
|
|
572
|
+
}
|
|
573
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
574
|
+
res.end(JSON.stringify({ success: true, message: 'Loop deleted' }));
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
579
|
+
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
// ==== TASK MANAGEMENT ENDPOINTS ====
|
|
584
|
+
// Helper to create TaskStorageManager instance
|
|
585
|
+
const createTaskManager = () => {
|
|
586
|
+
return new TaskStorageManager(workflowDir);
|
|
587
|
+
};
|
|
588
|
+
// POST /api/loops/v2/:loopId/tasks - Add task to loop
|
|
589
|
+
if (pathname.match(/\/api\/loops\/v2\/[^/]+\/tasks$/) && req.method === 'POST') {
|
|
590
|
+
const loopId = pathname.split('/').slice(-2)[0];
|
|
591
|
+
if (!loopId || !isValidId(loopId)) {
|
|
592
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
593
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' }));
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
handlePostRequest(req, res, async (body) => {
|
|
597
|
+
const { description, tool, mode, prompt_template, command, on_error } = body;
|
|
598
|
+
// Validation
|
|
599
|
+
if (!description || typeof description !== 'string' || description.trim().length === 0) {
|
|
600
|
+
return { success: false, error: 'description is required', status: 400 };
|
|
601
|
+
}
|
|
602
|
+
if (!tool || typeof tool !== 'string') {
|
|
603
|
+
return { success: false, error: 'tool is required', status: 400 };
|
|
604
|
+
}
|
|
605
|
+
// Get enabled tools from cli-tools.json dynamically
|
|
606
|
+
const cliToolsConfig = loadClaudeCliTools(os.homedir());
|
|
607
|
+
const enabledTools = Object.entries(cliToolsConfig.tools || {})
|
|
608
|
+
.filter(([_, config]) => config.enabled === true)
|
|
609
|
+
.map(([name]) => name);
|
|
610
|
+
// Also allow 'bash' as a special case (built-in tool)
|
|
611
|
+
const validTools = ['bash', ...enabledTools];
|
|
612
|
+
if (!validTools.includes(tool)) {
|
|
613
|
+
return { success: false, error: `tool must be one of enabled tools: ${validTools.join(', ')}`, status: 400 };
|
|
614
|
+
}
|
|
615
|
+
if (!mode || typeof mode !== 'string') {
|
|
616
|
+
return { success: false, error: 'mode is required', status: 400 };
|
|
617
|
+
}
|
|
618
|
+
const validModes = ['analysis', 'write', 'review'];
|
|
619
|
+
if (!validModes.includes(mode)) {
|
|
620
|
+
return { success: false, error: `mode must be one of: ${validModes.join(', ')}`, status: 400 };
|
|
621
|
+
}
|
|
622
|
+
if (!prompt_template || typeof prompt_template !== 'string' || prompt_template.trim().length === 0) {
|
|
623
|
+
return { success: false, error: 'prompt_template is required', status: 400 };
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
const taskManager = createTaskManager();
|
|
627
|
+
const task = await taskManager.addTask(loopId, {
|
|
628
|
+
description: description.trim(),
|
|
629
|
+
tool,
|
|
630
|
+
mode,
|
|
631
|
+
prompt_template: prompt_template.trim(),
|
|
632
|
+
command,
|
|
633
|
+
on_error
|
|
634
|
+
});
|
|
635
|
+
// Broadcast task added
|
|
636
|
+
try {
|
|
637
|
+
broadcastToClients({
|
|
638
|
+
type: 'TASK_ADDED',
|
|
639
|
+
loop_id: loopId,
|
|
640
|
+
task_id: task.task_id,
|
|
641
|
+
task: task
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
// Ignore broadcast errors
|
|
646
|
+
}
|
|
647
|
+
return { success: true, data: task };
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
return { success: false, error: error.message, status: 500 };
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
// GET /api/loops/v2/:loopId/tasks - List all tasks for loop
|
|
656
|
+
if (pathname.match(/\/api\/loops\/v2\/[^/]+\/tasks$/) && req.method === 'GET') {
|
|
657
|
+
const loopId = pathname.split('/').slice(-2)[0];
|
|
658
|
+
if (!loopId || !isValidId(loopId)) {
|
|
659
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
660
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' }));
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
const taskManager = createTaskManager();
|
|
665
|
+
const tasks = await taskManager.getTasks(loopId);
|
|
666
|
+
// Sort by order
|
|
667
|
+
tasks.sort((a, b) => a.order - b.order);
|
|
668
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
669
|
+
res.end(JSON.stringify({
|
|
670
|
+
success: true,
|
|
671
|
+
data: tasks,
|
|
672
|
+
total: tasks.length,
|
|
673
|
+
loop_id: loopId,
|
|
674
|
+
timestamp: new Date().toISOString()
|
|
675
|
+
}));
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
680
|
+
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
// GET /api/loops/v2/tasks/:taskId - Get single task (taskId lookup)
|
|
685
|
+
if (pathname.match(/^\/api\/loops\/v2\/tasks\/[^/]+$/) && req.method === 'GET') {
|
|
686
|
+
const taskId = pathname.split('/').pop();
|
|
687
|
+
if (!taskId || !isValidId(taskId)) {
|
|
688
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
689
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid task ID format' }));
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
const taskManager = createTaskManager();
|
|
694
|
+
// Get all loops and search for the task
|
|
695
|
+
const loops = await listLoops();
|
|
696
|
+
let foundTask = null;
|
|
697
|
+
let foundLoopId = null;
|
|
698
|
+
for (const loop of loops) {
|
|
699
|
+
const loopId = loop.loop_id;
|
|
700
|
+
try {
|
|
701
|
+
const tasks = await taskManager.getTasks(loopId);
|
|
702
|
+
const task = tasks.find(t => t.task_id === taskId);
|
|
703
|
+
if (task) {
|
|
704
|
+
foundTask = task;
|
|
705
|
+
foundLoopId = loopId;
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (!foundTask) {
|
|
714
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
715
|
+
res.end(JSON.stringify({ success: false, error: 'Task not found' }));
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
719
|
+
res.end(JSON.stringify({
|
|
720
|
+
success: true,
|
|
721
|
+
data: { ...foundTask, loop_id: foundLoopId },
|
|
722
|
+
timestamp: new Date().toISOString()
|
|
723
|
+
}));
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
728
|
+
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// PUT /api/loops/v2/tasks/:taskId - Update task (taskId lookup)
|
|
733
|
+
if (pathname.match(/^\/api\/loops\/v2\/tasks\/[^/]+$/) && req.method === 'PUT') {
|
|
734
|
+
const taskId = pathname.split('/').pop();
|
|
735
|
+
if (!taskId || !isValidId(taskId)) {
|
|
736
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
737
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid task ID format' }));
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
handlePostRequest(req, res, async (body) => {
|
|
741
|
+
const { loop_id, description, tool, mode, prompt_template, command, on_error } = body;
|
|
742
|
+
if (!loop_id || typeof loop_id !== 'string') {
|
|
743
|
+
return { success: false, error: 'loop_id is required', status: 400 };
|
|
744
|
+
}
|
|
745
|
+
if (!isValidId(loop_id)) {
|
|
746
|
+
return { success: false, error: 'Invalid loop_id format', status: 400 };
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
const taskManager = createTaskManager();
|
|
750
|
+
const updatedTask = await taskManager.updateTask(loop_id, taskId, {
|
|
751
|
+
description,
|
|
752
|
+
tool,
|
|
753
|
+
mode,
|
|
754
|
+
prompt_template,
|
|
755
|
+
command,
|
|
756
|
+
on_error
|
|
757
|
+
});
|
|
758
|
+
if (!updatedTask) {
|
|
759
|
+
return { success: false, error: 'Task not found', status: 404 };
|
|
760
|
+
}
|
|
761
|
+
// Broadcast task updated
|
|
762
|
+
try {
|
|
763
|
+
broadcastToClients({
|
|
764
|
+
type: 'TASK_UPDATED',
|
|
765
|
+
loop_id: loop_id,
|
|
766
|
+
task_id: taskId,
|
|
767
|
+
task: updatedTask
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
// Ignore broadcast errors
|
|
772
|
+
}
|
|
773
|
+
return { success: true, data: updatedTask };
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
return { success: false, error: error.message, status: 500 };
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
// DELETE /api/loops/v2/tasks/:taskId - Delete task (taskId lookup)
|
|
782
|
+
if (pathname.match(/^\/api\/loops\/v2\/tasks\/[^/]+$/) && req.method === 'DELETE') {
|
|
783
|
+
const taskId = pathname.split('/').pop();
|
|
784
|
+
if (!taskId || !isValidId(taskId)) {
|
|
785
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
786
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid task ID format' }));
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
// Get loop_id from query parameter
|
|
790
|
+
const urlObj = new URL(req.url || '', `http://localhost`);
|
|
791
|
+
const loopId = urlObj.searchParams.get('loop_id');
|
|
792
|
+
if (!loopId || !isValidId(loopId)) {
|
|
793
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
794
|
+
res.end(JSON.stringify({ success: false, error: 'loop_id query parameter is required' }));
|
|
795
|
+
return true;
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
const taskManager = createTaskManager();
|
|
799
|
+
const deleted = await taskManager.deleteTask(loopId, taskId);
|
|
800
|
+
if (!deleted) {
|
|
801
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
802
|
+
res.end(JSON.stringify({ success: false, error: 'Task not found' }));
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
// Broadcast task deleted
|
|
806
|
+
try {
|
|
807
|
+
broadcastToClients({
|
|
808
|
+
type: 'TASK_DELETED',
|
|
809
|
+
loop_id: loopId,
|
|
810
|
+
task_id: taskId
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
// Ignore broadcast errors
|
|
815
|
+
}
|
|
816
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
817
|
+
res.end(JSON.stringify({ success: true, message: 'Task deleted' }));
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
catch (error) {
|
|
821
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
822
|
+
res.end(JSON.stringify({ success: false, error: error.message }));
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
// PUT /api/loops/v2/:loopId/tasks/reorder - Reorder tasks
|
|
827
|
+
if (pathname.match(/\/api\/loops\/v2\/[^/]+\/tasks\/reorder$/) && req.method === 'PUT') {
|
|
828
|
+
const loopId = pathname.split('/').slice(-3)[0];
|
|
829
|
+
if (!loopId || !isValidId(loopId)) {
|
|
830
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
831
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' }));
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
handlePostRequest(req, res, async (body) => {
|
|
835
|
+
const { ordered_task_ids } = body;
|
|
836
|
+
if (!ordered_task_ids || !Array.isArray(ordered_task_ids)) {
|
|
837
|
+
return { success: false, error: 'ordered_task_ids must be an array', status: 400 };
|
|
838
|
+
}
|
|
839
|
+
if (ordered_task_ids.length === 0) {
|
|
840
|
+
return { success: false, error: 'ordered_task_ids cannot be empty', status: 400 };
|
|
841
|
+
}
|
|
842
|
+
try {
|
|
843
|
+
const taskManager = createTaskManager();
|
|
844
|
+
const reorderedTasks = await taskManager.reorderTasks(loopId, { ordered_task_ids });
|
|
845
|
+
// Broadcast tasks reordered
|
|
846
|
+
try {
|
|
847
|
+
broadcastToClients({
|
|
848
|
+
type: 'TASK_REORDERED',
|
|
849
|
+
loop_id: loopId,
|
|
850
|
+
ordered_task_ids: ordered_task_ids,
|
|
851
|
+
tasks: reorderedTasks
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
catch {
|
|
855
|
+
// Ignore broadcast errors
|
|
856
|
+
}
|
|
857
|
+
return { success: true, data: reorderedTasks };
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
return { success: false, error: error.message, status: 500 };
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
return true;
|
|
864
|
+
}
|
|
865
|
+
// ==== ADVANCED TASK FEATURES ====
|
|
866
|
+
// POST /api/loops/v2/:loopId/import - Import tasks from issue
|
|
867
|
+
if (pathname.match(/\/api\/loops\/v2\/[^/]+\/import$/) && req.method === 'POST') {
|
|
868
|
+
const loopId = pathname.split('/').slice(-2)[0];
|
|
869
|
+
if (!loopId || !isValidId(loopId)) {
|
|
870
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
871
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' }));
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
handlePostRequest(req, res, async (body) => {
|
|
875
|
+
const { issue_id } = body;
|
|
876
|
+
if (!issue_id || typeof issue_id !== 'string') {
|
|
877
|
+
return { success: false, error: 'issue_id is required', status: 400 };
|
|
878
|
+
}
|
|
879
|
+
try {
|
|
880
|
+
// Fetch issue data from issue-manager
|
|
881
|
+
const { readFile } = await import('fs/promises');
|
|
882
|
+
const { existsSync } = await import('fs');
|
|
883
|
+
const issuesDir = join(workflowDir, '.workflow', 'issues');
|
|
884
|
+
const issuesPath = join(issuesDir, 'issues.jsonl');
|
|
885
|
+
let issueData = null;
|
|
886
|
+
// Try reading from active issues
|
|
887
|
+
if (existsSync(issuesPath)) {
|
|
888
|
+
const content = await readFile(issuesPath, 'utf-8');
|
|
889
|
+
const issues = content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
|
|
890
|
+
issueData = issues.find((i) => i.id === issue_id);
|
|
891
|
+
}
|
|
892
|
+
// Try reading from history if not found
|
|
893
|
+
if (!issueData) {
|
|
894
|
+
const historyPath = join(issuesDir, 'issue-history.jsonl');
|
|
895
|
+
if (existsSync(historyPath)) {
|
|
896
|
+
const content = await readFile(historyPath, 'utf-8');
|
|
897
|
+
const historyIssues = content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
|
|
898
|
+
issueData = historyIssues.find((i) => i.id === issue_id);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (!issueData) {
|
|
902
|
+
return { success: false, error: `Issue ${issue_id} not found`, status: 404 };
|
|
903
|
+
}
|
|
904
|
+
// Load solutions to get bound solution tasks
|
|
905
|
+
const solutionsPath = join(issuesDir, 'solutions', `${issue_id}.jsonl`);
|
|
906
|
+
let tasksToImport = [];
|
|
907
|
+
if (existsSync(solutionsPath)) {
|
|
908
|
+
const solutionsContent = await readFile(solutionsPath, 'utf-8');
|
|
909
|
+
const solutions = solutionsContent.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
|
|
910
|
+
// Get tasks from bound solution
|
|
911
|
+
const boundSolution = solutions.find((s) => s.id === issueData.bound_solution_id) ||
|
|
912
|
+
solutions.find((s) => s.is_bound) ||
|
|
913
|
+
solutions[0];
|
|
914
|
+
if (boundSolution?.tasks) {
|
|
915
|
+
tasksToImport = boundSolution.tasks;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (tasksToImport.length === 0) {
|
|
919
|
+
return { success: false, error: 'No tasks found in issue. Bind a solution with tasks first.', status: 400 };
|
|
920
|
+
}
|
|
921
|
+
// Broadcast import start
|
|
922
|
+
broadcastToClients({
|
|
923
|
+
type: 'LOOP_TASK_IMPORT_PROGRESS',
|
|
924
|
+
loop_id: loopId,
|
|
925
|
+
payload: {
|
|
926
|
+
stage: 'starting',
|
|
927
|
+
total: tasksToImport.length,
|
|
928
|
+
imported: 0
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
const taskManager = createTaskManager();
|
|
932
|
+
const createdTasks = [];
|
|
933
|
+
// Convert issue tasks to loop tasks
|
|
934
|
+
for (let i = 0; i < tasksToImport.length; i++) {
|
|
935
|
+
const issueTask = tasksToImport[i];
|
|
936
|
+
// Map issue task fields to loop task fields
|
|
937
|
+
const taskRequest = {
|
|
938
|
+
description: issueTask.description || issueTask.title || `Task ${i + 1}`,
|
|
939
|
+
tool: mapIssueToolToLoopTool(issueTask.tool) || 'gemini',
|
|
940
|
+
mode: mapIssueModeToLoopMode(issueTask.mode) || 'write',
|
|
941
|
+
prompt_template: issueTask.prompt_template || issueTask.prompt || `Execute: ${issueTask.description || issueTask.title}`,
|
|
942
|
+
command: issueTask.command,
|
|
943
|
+
on_error: mapIssueOnError(issueTask.on_error)
|
|
944
|
+
};
|
|
945
|
+
const task = await taskManager.addTask(loopId, taskRequest);
|
|
946
|
+
createdTasks.push(task);
|
|
947
|
+
// Broadcast progress
|
|
948
|
+
broadcastToClients({
|
|
949
|
+
type: 'LOOP_TASK_IMPORT_PROGRESS',
|
|
950
|
+
loop_id: loopId,
|
|
951
|
+
payload: {
|
|
952
|
+
stage: 'importing',
|
|
953
|
+
total: tasksToImport.length,
|
|
954
|
+
imported: i + 1,
|
|
955
|
+
current_task: task
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
// Broadcast completion
|
|
960
|
+
broadcastToClients({
|
|
961
|
+
type: 'LOOP_TASK_IMPORT_COMPLETE',
|
|
962
|
+
loop_id: loopId,
|
|
963
|
+
payload: {
|
|
964
|
+
total: tasksToImport.length,
|
|
965
|
+
imported: createdTasks.length,
|
|
966
|
+
tasks: createdTasks
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
return {
|
|
970
|
+
success: true,
|
|
971
|
+
data: createdTasks,
|
|
972
|
+
message: `Imported ${createdTasks.length} tasks from issue ${issue_id}`
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
catch (error) {
|
|
976
|
+
return { success: false, error: error.message, status: 500 };
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
return true;
|
|
980
|
+
}
|
|
981
|
+
// POST /api/loops/v2/:loopId/generate - Generate tasks via Gemini
|
|
982
|
+
if (pathname.match(/\/api\/loops\/v2\/[^/]+\/generate$/) && req.method === 'POST') {
|
|
983
|
+
const loopId = pathname.split('/').slice(-2)[0];
|
|
984
|
+
if (!loopId || !isValidId(loopId)) {
|
|
985
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
986
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid loop ID format' }));
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
handlePostRequest(req, res, async (body) => {
|
|
990
|
+
const { tool = 'gemini', count } = body;
|
|
991
|
+
try {
|
|
992
|
+
// Get loop details for context
|
|
993
|
+
const loop = await readLoopStorage(loopId);
|
|
994
|
+
if (!loop) {
|
|
995
|
+
return { success: false, error: 'Loop not found', status: 404 };
|
|
996
|
+
}
|
|
997
|
+
// Broadcast generation start
|
|
998
|
+
broadcastToClients({
|
|
999
|
+
type: 'LOOP_TASK_GENERATION_PROGRESS',
|
|
1000
|
+
loop_id: loopId,
|
|
1001
|
+
payload: {
|
|
1002
|
+
stage: 'analyzing',
|
|
1003
|
+
message: 'Analyzing loop description...'
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
// Build generation prompt
|
|
1007
|
+
const generatePrompt = `PURPOSE: Generate ${count || 5} specific tasks for loop execution
|
|
1008
|
+
TASK: Analyze the loop description and generate a list of actionable tasks that can be executed via CLI tools. Each task should have clear description, tool selection, mode, and prompt template.
|
|
1009
|
+
MODE: analysis
|
|
1010
|
+
CONTEXT: Loop title: ${loop.title}
|
|
1011
|
+
Loop description: ${loop.description || 'No description provided'}
|
|
1012
|
+
Max iterations: ${loop.max_iterations}
|
|
1013
|
+
EXPECTED: Return a JSON array of tasks with this exact structure:
|
|
1014
|
+
[
|
|
1015
|
+
{
|
|
1016
|
+
"description": "Clear task description",
|
|
1017
|
+
"tool": "gemini|codex|qwen|bash",
|
|
1018
|
+
"mode": "analysis|write|review",
|
|
1019
|
+
"prompt_template": "PURPOSE: ... TASK: ... MODE: analysis CONTEXT: @**/* EXPECTED: ...",
|
|
1020
|
+
"on_error": "continue|pause|fail_fast"
|
|
1021
|
+
}
|
|
1022
|
+
]
|
|
1023
|
+
CONSTRAINTS: Generate ${count || 5} tasks | Use gemini for AI tasks | Use bash for CLI commands | Include error handling strategy`;
|
|
1024
|
+
// Call CLI with gemini to generate tasks
|
|
1025
|
+
let generatedTasks = [];
|
|
1026
|
+
let outputBuffer = '';
|
|
1027
|
+
const result = await executeCliTool({
|
|
1028
|
+
tool: tool === 'codex' || tool === 'qwen' || tool === 'gemini' ? tool : 'gemini',
|
|
1029
|
+
prompt: generatePrompt,
|
|
1030
|
+
mode: 'analysis',
|
|
1031
|
+
format: 'plain',
|
|
1032
|
+
cd: workflowDir,
|
|
1033
|
+
timeout: 120000, // 2 minutes timeout
|
|
1034
|
+
stream: true
|
|
1035
|
+
}, (unit) => {
|
|
1036
|
+
// Collect output
|
|
1037
|
+
outputBuffer += unit.content;
|
|
1038
|
+
// Broadcast partial output for progress
|
|
1039
|
+
broadcastToClients({
|
|
1040
|
+
type: 'LOOP_TASK_GENERATION_PROGRESS',
|
|
1041
|
+
loop_id: loopId,
|
|
1042
|
+
payload: {
|
|
1043
|
+
stage: 'generating',
|
|
1044
|
+
message: 'Generating tasks...',
|
|
1045
|
+
output: unit.content
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
});
|
|
1049
|
+
if (!result.success) {
|
|
1050
|
+
return { success: false, error: 'Failed to generate tasks via CLI', status: 500 };
|
|
1051
|
+
}
|
|
1052
|
+
// Parse generated tasks from CLI output
|
|
1053
|
+
generatedTasks = parseGeneratedTasks(outputBuffer);
|
|
1054
|
+
if (generatedTasks.length === 0) {
|
|
1055
|
+
return {
|
|
1056
|
+
success: false,
|
|
1057
|
+
error: 'No valid tasks generated. Check CLI output for details.',
|
|
1058
|
+
status: 500,
|
|
1059
|
+
output: outputBuffer
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
// Broadcast import start
|
|
1063
|
+
broadcastToClients({
|
|
1064
|
+
type: 'LOOP_TASK_GENERATION_PROGRESS',
|
|
1065
|
+
loop_id: loopId,
|
|
1066
|
+
payload: {
|
|
1067
|
+
stage: 'importing',
|
|
1068
|
+
message: `Importing ${generatedTasks.length} generated tasks...`,
|
|
1069
|
+
total: generatedTasks.length,
|
|
1070
|
+
imported: 0
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
const taskManager = createTaskManager();
|
|
1074
|
+
const createdTasks = [];
|
|
1075
|
+
// Add generated tasks to loop
|
|
1076
|
+
for (let i = 0; i < generatedTasks.length; i++) {
|
|
1077
|
+
const genTask = generatedTasks[i];
|
|
1078
|
+
const taskRequest = {
|
|
1079
|
+
description: genTask.description || `Generated Task ${i + 1}`,
|
|
1080
|
+
tool: validateTool(genTask.tool) ? genTask.tool : 'gemini',
|
|
1081
|
+
mode: validateMode(genTask.mode) ? genTask.mode : 'write',
|
|
1082
|
+
prompt_template: genTask.prompt_template || `Execute task: ${genTask.description}`,
|
|
1083
|
+
command: genTask.command,
|
|
1084
|
+
on_error: validateOnError(genTask.on_error) ? genTask.on_error : 'continue'
|
|
1085
|
+
};
|
|
1086
|
+
const task = await taskManager.addTask(loopId, taskRequest);
|
|
1087
|
+
createdTasks.push(task);
|
|
1088
|
+
// Broadcast progress
|
|
1089
|
+
broadcastToClients({
|
|
1090
|
+
type: 'LOOP_TASK_GENERATION_PROGRESS',
|
|
1091
|
+
loop_id: loopId,
|
|
1092
|
+
payload: {
|
|
1093
|
+
stage: 'importing',
|
|
1094
|
+
message: `Importing task ${i + 1}/${generatedTasks.length}...`,
|
|
1095
|
+
total: generatedTasks.length,
|
|
1096
|
+
imported: i + 1,
|
|
1097
|
+
current_task: task
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
// Broadcast completion
|
|
1102
|
+
broadcastToClients({
|
|
1103
|
+
type: 'LOOP_TASK_GENERATION_COMPLETE',
|
|
1104
|
+
loop_id: loopId,
|
|
1105
|
+
payload: {
|
|
1106
|
+
total: generatedTasks.length,
|
|
1107
|
+
imported: createdTasks.length,
|
|
1108
|
+
tasks: createdTasks
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
return {
|
|
1112
|
+
success: true,
|
|
1113
|
+
data: createdTasks,
|
|
1114
|
+
message: `Generated and imported ${createdTasks.length} tasks`
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
return { success: false, error: error.message, status: 500 };
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
return true;
|
|
1122
|
+
}
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Sanitize ID parameter to prevent path traversal attacks
|
|
1127
|
+
* @returns true if valid, false if invalid
|
|
1128
|
+
*/
|
|
1129
|
+
function isValidId(id) {
|
|
1130
|
+
if (!id)
|
|
1131
|
+
return false;
|
|
1132
|
+
// Block path traversal attempts and null bytes
|
|
1133
|
+
if (id.includes('/') || id.includes('\\') || id === '..' || id === '.')
|
|
1134
|
+
return false;
|
|
1135
|
+
if (id.includes('\0'))
|
|
1136
|
+
return false;
|
|
1137
|
+
return true;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Get enabled tools list from cache
|
|
1141
|
+
* If cache is not initialized, it will load from config (fallback for lazy initialization)
|
|
1142
|
+
*/
|
|
1143
|
+
function getEnabledToolsList() {
|
|
1144
|
+
// Return cached value if available
|
|
1145
|
+
if (cachedEnabledTools) {
|
|
1146
|
+
return cachedEnabledTools;
|
|
1147
|
+
}
|
|
1148
|
+
// Fallback: lazy initialization if cache not initialized (shouldn't happen in normal operation)
|
|
1149
|
+
console.warn('[Loop V2] CLI tools cache not initialized, performing lazy load');
|
|
1150
|
+
initializeCliToolsCache();
|
|
1151
|
+
return cachedEnabledTools || ['bash', 'gemini', 'qwen', 'codex', 'claude'];
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Map issue tool to loop tool
|
|
1155
|
+
*/
|
|
1156
|
+
function mapIssueToolToLoopTool(tool) {
|
|
1157
|
+
const validTools = getEnabledToolsList();
|
|
1158
|
+
if (validTools.includes(tool))
|
|
1159
|
+
return tool;
|
|
1160
|
+
// Map aliases
|
|
1161
|
+
if (tool === 'ccw')
|
|
1162
|
+
return 'gemini';
|
|
1163
|
+
if (tool === 'ai')
|
|
1164
|
+
return 'gemini';
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Map issue mode to loop mode
|
|
1169
|
+
*/
|
|
1170
|
+
function mapIssueModeToLoopMode(mode) {
|
|
1171
|
+
const validModes = ['analysis', 'write', 'review'];
|
|
1172
|
+
if (validModes.includes(mode))
|
|
1173
|
+
return mode;
|
|
1174
|
+
// Map aliases
|
|
1175
|
+
if (mode === 'read')
|
|
1176
|
+
return 'analysis';
|
|
1177
|
+
if (mode === 'create' || mode === 'modify')
|
|
1178
|
+
return 'write';
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Map issue on_error value
|
|
1183
|
+
*/
|
|
1184
|
+
function mapIssueOnError(onError) {
|
|
1185
|
+
const validValues = ['continue', 'pause', 'fail_fast'];
|
|
1186
|
+
if (validValues.includes(onError))
|
|
1187
|
+
return onError;
|
|
1188
|
+
// Map aliases
|
|
1189
|
+
if (onError === 'stop')
|
|
1190
|
+
return 'pause';
|
|
1191
|
+
if (onError === 'abort')
|
|
1192
|
+
return 'fail_fast';
|
|
1193
|
+
return undefined;
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Validate tool value
|
|
1197
|
+
*/
|
|
1198
|
+
function validateTool(tool) {
|
|
1199
|
+
const validTools = getEnabledToolsList();
|
|
1200
|
+
return validTools.includes(tool);
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Validate mode value
|
|
1204
|
+
*/
|
|
1205
|
+
function validateMode(mode) {
|
|
1206
|
+
const validModes = ['analysis', 'write', 'review'];
|
|
1207
|
+
return validModes.includes(mode);
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Validate on_error value
|
|
1211
|
+
*/
|
|
1212
|
+
function validateOnError(onError) {
|
|
1213
|
+
const validValues = ['continue', 'pause', 'fail_fast'];
|
|
1214
|
+
return validValues.includes(onError);
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Parse generated tasks from CLI output
|
|
1218
|
+
* Extracts JSON array from output, handles various response formats
|
|
1219
|
+
*/
|
|
1220
|
+
function parseGeneratedTasks(output) {
|
|
1221
|
+
let tasks = [];
|
|
1222
|
+
// Try to find JSON array in output
|
|
1223
|
+
const jsonMatch = output.match(/\[[\s\S]*\]/);
|
|
1224
|
+
if (jsonMatch) {
|
|
1225
|
+
try {
|
|
1226
|
+
tasks = JSON.parse(jsonMatch[0]);
|
|
1227
|
+
}
|
|
1228
|
+
catch {
|
|
1229
|
+
// Invalid JSON, try alternative parsing
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
// If no valid JSON array found, try parsing line by line
|
|
1233
|
+
if (tasks.length === 0) {
|
|
1234
|
+
const lines = output.split('\n');
|
|
1235
|
+
for (const line of lines) {
|
|
1236
|
+
const trimmed = line.trim();
|
|
1237
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
1238
|
+
try {
|
|
1239
|
+
tasks.push(JSON.parse(trimmed));
|
|
1240
|
+
}
|
|
1241
|
+
catch {
|
|
1242
|
+
// Skip invalid lines
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
// Filter and validate task objects
|
|
1248
|
+
return tasks.filter(t => t &&
|
|
1249
|
+
typeof t === 'object' &&
|
|
1250
|
+
(t.description || t.title || t.task) &&
|
|
1251
|
+
(t.tool || t.mode || t.prompt_template)).map(t => ({
|
|
1252
|
+
description: t.description || t.title || t.task || 'Untitled task',
|
|
1253
|
+
tool: t.tool || 'gemini',
|
|
1254
|
+
mode: t.mode || 'write',
|
|
1255
|
+
prompt_template: t.prompt_template || t.prompt || `Execute: ${t.description || t.title || t.task}`,
|
|
1256
|
+
command: t.command,
|
|
1257
|
+
on_error: t.on_error || 'continue'
|
|
1258
|
+
}));
|
|
1259
|
+
}
|
|
1260
|
+
//# sourceMappingURL=loop-v2-routes.js.map
|