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.
Files changed (250) hide show
  1. package/.claude/commands/workflow/lite-execute.md +2 -0
  2. package/.claude/commands/workflow/lite-fix.md +108 -9
  3. package/.claude/skills/ccw-loop/README.md +303 -0
  4. package/.claude/skills/ccw-loop/SKILL.md +259 -0
  5. package/.claude/skills/ccw-loop/phases/actions/action-complete.md +320 -0
  6. package/.claude/skills/ccw-loop/phases/actions/action-debug-with-file.md +485 -0
  7. package/.claude/skills/ccw-loop/phases/actions/action-develop-with-file.md +365 -0
  8. package/.claude/skills/ccw-loop/phases/actions/action-init.md +200 -0
  9. package/.claude/skills/ccw-loop/phases/actions/action-menu.md +192 -0
  10. package/.claude/skills/ccw-loop/phases/actions/action-validate-with-file.md +307 -0
  11. package/.claude/skills/ccw-loop/phases/orchestrator.md +486 -0
  12. package/.claude/skills/ccw-loop/phases/state-schema.md +474 -0
  13. package/.claude/skills/ccw-loop/specs/action-catalog.md +300 -0
  14. package/.claude/skills/ccw-loop/specs/loop-requirements.md +192 -0
  15. package/.claude/skills/ccw-loop/templates/progress-template.md +175 -0
  16. package/.claude/skills/ccw-loop/templates/understanding-template.md +303 -0
  17. package/.claude/skills/ccw-loop/templates/validation-template.md +258 -0
  18. package/.codex/agents/action-planning-agent.md +885 -0
  19. package/.codex/agents/ccw-loop-executor.md +260 -0
  20. package/.codex/agents/cli-discuss-agent.md +391 -0
  21. package/.codex/agents/cli-execution-agent.md +333 -0
  22. package/.codex/agents/cli-explore-agent.md +186 -0
  23. package/.codex/agents/cli-lite-planning-agent.md +736 -0
  24. package/.codex/agents/cli-planning-agent.md +562 -0
  25. package/.codex/agents/code-developer.md +408 -0
  26. package/.codex/agents/conceptual-planning-agent.md +321 -0
  27. package/.codex/agents/context-search-agent.md +585 -0
  28. package/.codex/agents/debug-explore-agent.md +436 -0
  29. package/.codex/agents/doc-generator.md +334 -0
  30. package/.codex/agents/issue-plan-agent.md +417 -0
  31. package/.codex/agents/issue-queue-agent.md +311 -0
  32. package/.codex/agents/memory-bridge.md +96 -0
  33. package/.codex/agents/test-context-search-agent.md +402 -0
  34. package/.codex/agents/test-fix-agent.md +359 -0
  35. package/.codex/agents/ui-design-agent.md +595 -0
  36. package/.codex/agents/universal-executor.md +135 -0
  37. package/.codex/prompts/issue-discover-by-prompt.md +364 -0
  38. package/.codex/prompts/issue-discover.md +261 -0
  39. package/.codex/prompts/issue-execute.md +10 -0
  40. package/.codex/prompts/issue-new.md +285 -0
  41. package/.codex/prompts/issue-plan.md +161 -63
  42. package/.codex/prompts/issue-queue.md +298 -288
  43. package/.codex/prompts/lite-execute.md +627 -133
  44. package/.codex/prompts/lite-fix.md +670 -0
  45. package/.codex/prompts/lite-plan-a.md +337 -0
  46. package/.codex/prompts/lite-plan-b.md +485 -0
  47. package/.codex/prompts/{lite-plan.md → lite-plan-c.md} +601 -469
  48. package/.codex/skills/ccw-loop/README.md +171 -0
  49. package/.codex/skills/ccw-loop/SKILL.md +349 -0
  50. package/.codex/skills/ccw-loop/phases/actions/action-complete.md +269 -0
  51. package/.codex/skills/ccw-loop/phases/actions/action-debug.md +286 -0
  52. package/.codex/skills/ccw-loop/phases/actions/action-develop.md +183 -0
  53. package/.codex/skills/ccw-loop/phases/actions/action-init.md +164 -0
  54. package/.codex/skills/ccw-loop/phases/actions/action-menu.md +205 -0
  55. package/.codex/skills/ccw-loop/phases/actions/action-validate.md +250 -0
  56. package/.codex/skills/ccw-loop/phases/orchestrator.md +416 -0
  57. package/.codex/skills/ccw-loop/phases/state-schema.md +388 -0
  58. package/.codex/skills/ccw-loop/specs/action-catalog.md +182 -0
  59. package/.codex/skills/ccw-loop-b/README.md +102 -0
  60. package/.codex/skills/ccw-loop-b/SKILL.md +322 -0
  61. package/.codex/skills/ccw-loop-b/phases/orchestrator.md +257 -0
  62. package/.codex/skills/ccw-loop-b/phases/state-schema.md +181 -0
  63. package/ccw/dist/cli.d.ts.map +1 -1
  64. package/ccw/dist/cli.js +12 -1
  65. package/ccw/dist/cli.js.map +1 -1
  66. package/ccw/dist/commands/cli.d.ts.map +1 -1
  67. package/ccw/dist/commands/cli.js +14 -1
  68. package/ccw/dist/commands/cli.js.map +1 -1
  69. package/ccw/dist/commands/install.d.ts.map +1 -1
  70. package/ccw/dist/commands/install.js +38 -7
  71. package/ccw/dist/commands/install.js.map +1 -1
  72. package/ccw/dist/commands/issue.d.ts +3 -0
  73. package/ccw/dist/commands/issue.d.ts.map +1 -1
  74. package/ccw/dist/commands/issue.js +107 -0
  75. package/ccw/dist/commands/issue.js.map +1 -1
  76. package/ccw/dist/commands/loop.d.ts +10 -0
  77. package/ccw/dist/commands/loop.d.ts.map +1 -0
  78. package/ccw/dist/commands/loop.js +289 -0
  79. package/ccw/dist/commands/loop.js.map +1 -0
  80. package/ccw/dist/commands/upgrade.js +1 -1
  81. package/ccw/dist/commands/upgrade.js.map +1 -1
  82. package/ccw/dist/config/litellm-api-config-manager.d.ts.map +1 -1
  83. package/ccw/dist/config/litellm-api-config-manager.js +3 -2
  84. package/ccw/dist/config/litellm-api-config-manager.js.map +1 -1
  85. package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
  86. package/ccw/dist/core/dashboard-generator.js +4 -1
  87. package/ccw/dist/core/dashboard-generator.js.map +1 -1
  88. package/ccw/dist/core/memory-embedder-bridge.d.ts.map +1 -1
  89. package/ccw/dist/core/memory-embedder-bridge.js +2 -5
  90. package/ccw/dist/core/memory-embedder-bridge.js.map +1 -1
  91. package/ccw/dist/core/routes/claude-routes.d.ts.map +1 -1
  92. package/ccw/dist/core/routes/claude-routes.js +5 -3
  93. package/ccw/dist/core/routes/claude-routes.js.map +1 -1
  94. package/ccw/dist/core/routes/cli-routes.d.ts +6 -0
  95. package/ccw/dist/core/routes/cli-routes.d.ts.map +1 -1
  96. package/ccw/dist/core/routes/cli-routes.js +42 -13
  97. package/ccw/dist/core/routes/cli-routes.js.map +1 -1
  98. package/ccw/dist/core/routes/cli-settings-routes.d.ts.map +1 -1
  99. package/ccw/dist/core/routes/cli-settings-routes.js +44 -0
  100. package/ccw/dist/core/routes/cli-settings-routes.js.map +1 -1
  101. package/ccw/dist/core/routes/codexlens/config-handlers.d.ts.map +1 -1
  102. package/ccw/dist/core/routes/codexlens/config-handlers.js +7 -6
  103. package/ccw/dist/core/routes/codexlens/config-handlers.js.map +1 -1
  104. package/ccw/dist/core/routes/codexlens/semantic-handlers.d.ts.map +1 -1
  105. package/ccw/dist/core/routes/codexlens/semantic-handlers.js +5 -4
  106. package/ccw/dist/core/routes/codexlens/semantic-handlers.js.map +1 -1
  107. package/ccw/dist/core/routes/core-memory-routes.d.ts.map +1 -1
  108. package/ccw/dist/core/routes/core-memory-routes.js +4 -2
  109. package/ccw/dist/core/routes/core-memory-routes.js.map +1 -1
  110. package/ccw/dist/core/routes/files-routes.d.ts.map +1 -1
  111. package/ccw/dist/core/routes/files-routes.js +4 -2
  112. package/ccw/dist/core/routes/files-routes.js.map +1 -1
  113. package/ccw/dist/core/routes/graph-routes.d.ts.map +1 -1
  114. package/ccw/dist/core/routes/graph-routes.js +17 -2
  115. package/ccw/dist/core/routes/graph-routes.js.map +1 -1
  116. package/ccw/dist/core/routes/issue-routes.d.ts.map +1 -1
  117. package/ccw/dist/core/routes/issue-routes.js +280 -33
  118. package/ccw/dist/core/routes/issue-routes.js.map +1 -1
  119. package/ccw/dist/core/routes/loop-routes.d.ts +24 -0
  120. package/ccw/dist/core/routes/loop-routes.d.ts.map +1 -0
  121. package/ccw/dist/core/routes/loop-routes.js +334 -0
  122. package/ccw/dist/core/routes/loop-routes.js.map +1 -0
  123. package/ccw/dist/core/routes/loop-v2-routes.d.ts +44 -0
  124. package/ccw/dist/core/routes/loop-v2-routes.d.ts.map +1 -0
  125. package/ccw/dist/core/routes/loop-v2-routes.js +1260 -0
  126. package/ccw/dist/core/routes/loop-v2-routes.js.map +1 -0
  127. package/ccw/dist/core/routes/memory-routes.d.ts.map +1 -1
  128. package/ccw/dist/core/routes/memory-routes.js +2 -1
  129. package/ccw/dist/core/routes/memory-routes.js.map +1 -1
  130. package/ccw/dist/core/routes/system-routes.d.ts.map +1 -1
  131. package/ccw/dist/core/routes/system-routes.js +3 -2
  132. package/ccw/dist/core/routes/system-routes.js.map +1 -1
  133. package/ccw/dist/core/routes/task-routes.d.ts +12 -0
  134. package/ccw/dist/core/routes/task-routes.d.ts.map +1 -0
  135. package/ccw/dist/core/routes/task-routes.js +321 -0
  136. package/ccw/dist/core/routes/task-routes.js.map +1 -0
  137. package/ccw/dist/core/routes/test-loop-routes.d.ts +11 -0
  138. package/ccw/dist/core/routes/test-loop-routes.d.ts.map +1 -0
  139. package/ccw/dist/core/routes/test-loop-routes.js +298 -0
  140. package/ccw/dist/core/routes/test-loop-routes.js.map +1 -0
  141. package/ccw/dist/core/server.d.ts.map +1 -1
  142. package/ccw/dist/core/server.js +47 -5
  143. package/ccw/dist/core/server.js.map +1 -1
  144. package/ccw/dist/core/websocket.d.ts +59 -0
  145. package/ccw/dist/core/websocket.d.ts.map +1 -1
  146. package/ccw/dist/core/websocket.js +34 -0
  147. package/ccw/dist/core/websocket.js.map +1 -1
  148. package/ccw/dist/tools/claude-cli-tools.d.ts +40 -0
  149. package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
  150. package/ccw/dist/tools/claude-cli-tools.js +119 -0
  151. package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
  152. package/ccw/dist/tools/codex-lens-lsp.d.ts.map +1 -1
  153. package/ccw/dist/tools/codex-lens-lsp.js +2 -5
  154. package/ccw/dist/tools/codex-lens-lsp.js.map +1 -1
  155. package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
  156. package/ccw/dist/tools/codex-lens.js +22 -32
  157. package/ccw/dist/tools/codex-lens.js.map +1 -1
  158. package/ccw/dist/tools/litellm-client.d.ts +6 -0
  159. package/ccw/dist/tools/litellm-client.d.ts.map +1 -1
  160. package/ccw/dist/tools/litellm-client.js +15 -2
  161. package/ccw/dist/tools/litellm-client.js.map +1 -1
  162. package/ccw/dist/tools/loop-manager.d.ts +84 -0
  163. package/ccw/dist/tools/loop-manager.d.ts.map +1 -0
  164. package/ccw/dist/tools/loop-manager.js +425 -0
  165. package/ccw/dist/tools/loop-manager.js.map +1 -0
  166. package/ccw/dist/tools/loop-state-manager.d.ts +47 -0
  167. package/ccw/dist/tools/loop-state-manager.d.ts.map +1 -0
  168. package/ccw/dist/tools/loop-state-manager.js +149 -0
  169. package/ccw/dist/tools/loop-state-manager.js.map +1 -0
  170. package/ccw/dist/tools/loop-task-manager.d.ts +149 -0
  171. package/ccw/dist/tools/loop-task-manager.d.ts.map +1 -0
  172. package/ccw/dist/tools/loop-task-manager.js +270 -0
  173. package/ccw/dist/tools/loop-task-manager.js.map +1 -0
  174. package/ccw/dist/tools/native-session-discovery.d.ts.map +1 -1
  175. package/ccw/dist/tools/native-session-discovery.js +35 -7
  176. package/ccw/dist/tools/native-session-discovery.js.map +1 -1
  177. package/ccw/dist/types/index.d.ts +1 -0
  178. package/ccw/dist/types/index.d.ts.map +1 -1
  179. package/ccw/dist/types/index.js +1 -0
  180. package/ccw/dist/types/index.js.map +1 -1
  181. package/ccw/dist/types/loop.d.ts +257 -0
  182. package/ccw/dist/types/loop.d.ts.map +1 -0
  183. package/ccw/dist/types/loop.js +17 -0
  184. package/ccw/dist/types/loop.js.map +1 -0
  185. package/ccw/dist/utils/codexlens-path.d.ts +36 -0
  186. package/ccw/dist/utils/codexlens-path.d.ts.map +1 -0
  187. package/ccw/dist/utils/codexlens-path.js +56 -0
  188. package/ccw/dist/utils/codexlens-path.js.map +1 -0
  189. package/ccw/dist/utils/uv-manager.d.ts.map +1 -1
  190. package/ccw/dist/utils/uv-manager.js +3 -2
  191. package/ccw/dist/utils/uv-manager.js.map +1 -1
  192. package/ccw/src/cli.ts +13 -1
  193. package/ccw/src/commands/cli.ts +14 -1
  194. package/ccw/src/commands/install.ts +50 -7
  195. package/ccw/src/commands/issue.ts +119 -0
  196. package/ccw/src/commands/loop.ts +344 -0
  197. package/ccw/src/commands/upgrade.ts +1 -1
  198. package/ccw/src/config/litellm-api-config-manager.ts +3 -2
  199. package/ccw/src/core/dashboard-generator.ts +4 -1
  200. package/ccw/src/core/memory-embedder-bridge.ts +2 -6
  201. package/ccw/src/core/routes/claude-routes.ts +5 -3
  202. package/ccw/src/core/routes/cli-routes.ts +48 -16
  203. package/ccw/src/core/routes/cli-settings-routes.ts +47 -0
  204. package/ccw/src/core/routes/codexlens/config-handlers.ts +7 -6
  205. package/ccw/src/core/routes/codexlens/semantic-handlers.ts +5 -4
  206. package/ccw/src/core/routes/core-memory-routes.ts +4 -2
  207. package/ccw/src/core/routes/files-routes.ts +4 -2
  208. package/ccw/src/core/routes/graph-routes.ts +18 -2
  209. package/ccw/src/core/routes/issue-routes.ts +308 -33
  210. package/ccw/src/core/routes/loop-routes.ts +386 -0
  211. package/ccw/src/core/routes/loop-v2-routes.ts +1470 -0
  212. package/ccw/src/core/routes/memory-routes.ts +2 -1
  213. package/ccw/src/core/routes/system-routes.ts +3 -2
  214. package/ccw/src/core/routes/task-routes.ts +361 -0
  215. package/ccw/src/core/routes/test-loop-routes.ts +312 -0
  216. package/ccw/src/core/server.ts +49 -5
  217. package/ccw/src/core/websocket.ts +104 -0
  218. package/ccw/src/templates/dashboard-css/02-session.css +2 -0
  219. package/ccw/src/templates/dashboard-css/04-lite-tasks.css +103 -1
  220. package/ccw/src/templates/dashboard-css/12-cli-legacy.css +56 -0
  221. package/ccw/src/templates/dashboard-css/32-issue-manager.css +32 -0
  222. package/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +55 -0
  223. package/ccw/src/templates/dashboard-css/36-loop-monitor.css +1896 -0
  224. package/ccw/src/templates/dashboard-css/36-loop-monitor.css.backup +1877 -0
  225. package/ccw/src/templates/dashboard-js/components/cli-history.js +48 -48
  226. package/ccw/src/templates/dashboard-js/components/cli-status.js +64 -3
  227. package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +251 -110
  228. package/ccw/src/templates/dashboard-js/components/navigation.js +16 -0
  229. package/ccw/src/templates/dashboard-js/components/notifications.js +22 -0
  230. package/ccw/src/templates/dashboard-js/components/version-check.js +38 -0
  231. package/ccw/src/templates/dashboard-js/i18n.js +601 -1
  232. package/ccw/src/templates/dashboard-js/state.js +2 -0
  233. package/ccw/src/templates/dashboard-js/views/cli-manager.js +4 -3
  234. package/ccw/src/templates/dashboard-js/views/issue-manager.js +183 -1
  235. package/ccw/src/templates/dashboard-js/views/lite-tasks.js +55 -11
  236. package/ccw/src/templates/dashboard-js/views/loop-monitor.js +3345 -0
  237. package/ccw/src/templates/dashboard.html +68 -4
  238. package/ccw/src/tools/claude-cli-tools.ts +143 -0
  239. package/ccw/src/tools/codex-lens-lsp.ts +2 -5
  240. package/ccw/src/tools/codex-lens.ts +27 -38
  241. package/ccw/src/tools/litellm-client.ts +16 -2
  242. package/ccw/src/tools/loop-manager.ts +519 -0
  243. package/ccw/src/tools/loop-state-manager.ts +173 -0
  244. package/ccw/src/tools/loop-task-manager.ts +391 -0
  245. package/ccw/src/tools/native-session-discovery.ts +38 -7
  246. package/ccw/src/types/index.ts +1 -0
  247. package/ccw/src/types/loop.ts +316 -0
  248. package/ccw/src/utils/codexlens-path.ts +60 -0
  249. package/ccw/src/utils/uv-manager.ts +3 -2
  250. 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