@assistkick/create 1.10.0 → 1.11.0
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/dist/src/scaffolder.d.ts +12 -1
- package/dist/src/scaffolder.js +40 -3
- package/dist/src/scaffolder.js.map +1 -1
- package/package.json +1 -1
- package/templates/assistkick-product-system/package.json +1 -1
- package/templates/assistkick-product-system/packages/backend/package.json +1 -0
- package/templates/assistkick-product-system/packages/backend/src/mcp/permission_mcp_server.ts +196 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +31 -7
- package/templates/assistkick-product-system/packages/backend/src/routes/auth.ts +15 -12
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.test.ts +95 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.ts +97 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_permission.ts +94 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_sessions.ts +189 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.test.ts +131 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.ts +94 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +12 -3
- package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +2 -2
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +390 -22
- package/templates/assistkick-product-system/packages/backend/src/routes/git_branches.test.ts +306 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/git_connect.test.ts +133 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +66 -9
- package/templates/assistkick-product-system/packages/backend/src/routes/preview.ts +204 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.test.ts +205 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +37 -9
- package/templates/assistkick-product-system/packages/backend/src/routes/skills.test.ts +139 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/skills.ts +95 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +5 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/users.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +8 -8
- package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +5 -5
- package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +6 -6
- package/templates/assistkick-product-system/packages/backend/src/server.ts +107 -27
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +105 -203
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +76 -266
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.test.ts +427 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +345 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.test.ts +170 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.ts +106 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.test.ts +217 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.ts +188 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.test.ts +1243 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +894 -0
- package/templates/assistkick-product-system/packages/backend/src/services/coherence-review.ts +3 -3
- package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.test.ts +85 -0
- package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.ts +54 -0
- package/templates/assistkick-product-system/packages/backend/src/services/email_service.ts +13 -10
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +11 -3
- package/templates/assistkick-product-system/packages/backend/src/services/invitation_service.ts +1 -1
- package/templates/assistkick-product-system/packages/backend/src/services/password_reset_service.ts +1 -1
- package/templates/assistkick-product-system/packages/backend/src/services/permission_service.test.ts +243 -0
- package/templates/assistkick-product-system/packages/backend/src/services/permission_service.ts +259 -0
- package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.test.ts +172 -0
- package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.ts +225 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +29 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +17 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +255 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +300 -25
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +44 -0
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +62 -7
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +77 -6
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +129 -8
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +2 -1
- package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.test.ts +45 -0
- package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.ts +157 -0
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +4 -3
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +3 -3
- package/templates/assistkick-product-system/packages/frontend/package.json +5 -0
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +336 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +192 -12
- package/templates/assistkick-product-system/packages/frontend/src/components/AttachmentPreviewList.tsx +98 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/AutocompleteDropdown.tsx +65 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatAttachButton.tsx +56 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatDropZone.tsx +80 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx +155 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx +182 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageInput.tsx +233 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatSessionSidebar.tsx +218 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatStopButton.tsx +32 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatTodoSidebar.tsx +113 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +842 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/CommitMessageModal.tsx +82 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/DiagramOverlay.tsx +160 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +5 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +9 -10
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +5 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +112 -41
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/HighlightedText.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ImageLightbox.tsx +192 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/MentionPill.tsx +33 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/MermaidBlock.tsx +148 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/PermissionDialog.tsx +91 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/PermissionModeSelector.tsx +229 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +249 -83
- package/templates/assistkick-product-system/packages/frontend/src/components/QueuedMessageBubble.tsx +38 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +212 -117
- package/templates/assistkick-product-system/packages/frontend/src/components/SystemPromptAccordion.tsx +48 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/TaskIcon.tsx +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +25 -9
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolDiffView.tsx +114 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolResultCard.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +149 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +25 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/UnifiedGitWidget.tsx +722 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +2 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/ProgrammableNode.tsx +178 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +3 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +103 -9
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +26 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +42 -1
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useDocumentTitle.ts +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +1 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_chat_stream.ts +826 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_file_tree_cache.ts +69 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_mention_autocomplete.ts +284 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.test.ts +183 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.ts +150 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.test.ts +305 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.ts +113 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.test.ts +157 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.ts +95 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.test.ts +65 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.ts +110 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/message_queue.ts +66 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.test.ts +124 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.ts +112 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/ChatRoute.tsx +8 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +0 -4
- package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/accept_invitation.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/forgot_password.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/login.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/register.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/reset_password.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useAttachmentStore.ts +66 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useChatSessionStore.ts +107 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useMessageQueueStore.ts +110 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/usePreviewStore.ts +78 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +7 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +6 -1
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +30 -357
- package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.test.ts +115 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.ts +91 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.test.ts +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.ts +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
- package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
- package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
- package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
- package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
- package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
- package/templates/assistkick-product-system/packages/shared/package.json +2 -1
- package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
- package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
- package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
- package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
- package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
- package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
- package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
- package/templates/skills/assistkick-debugger/SKILL.md +23 -23
- package/templates/skills/assistkick-developer/SKILL.md +59 -63
- package/templates/skills/assistkick-interview/SKILL.md +26 -26
- package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
- package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { randomUUID } from 'node:crypto';
|
|
17
|
-
import {
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { resolve as pathResolve, dirname, join as pathJoin } from 'node:path';
|
|
18
19
|
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import type { ChildProcess } from 'node:child_process';
|
|
19
21
|
import { eq, and } from 'drizzle-orm';
|
|
20
22
|
import {
|
|
21
23
|
workflows,
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
agents,
|
|
26
28
|
nodes as nodesTable,
|
|
27
29
|
} from '../db/schema.js';
|
|
30
|
+
import { ProgrammableNodeExecutor } from './programmable_node_executor.js';
|
|
28
31
|
|
|
29
32
|
// ── Types ──────────────────────────────────────────────────────────────
|
|
30
33
|
|
|
@@ -86,7 +89,7 @@ interface WorkflowEngineDeps {
|
|
|
86
89
|
saveKanbanEntry: (featureId: string, entry: Record<string, unknown>, projectId?: string) => Promise<void>;
|
|
87
90
|
};
|
|
88
91
|
claudeService: {
|
|
89
|
-
spawnClaude: (prompt: string, cwd: string, label?: string, opts?: Record<string, unknown>) => Promise<string>;
|
|
92
|
+
spawnClaude: (prompt: string, cwd: string, label?: string, opts?: Record<string, unknown> & { sessionId?: string; resume?: string; model?: string }) => Promise<string>;
|
|
90
93
|
spawnCommand: (cmd: string, args: string[], cwd: string) => Promise<string>;
|
|
91
94
|
};
|
|
92
95
|
gitWorkflow: {
|
|
@@ -100,11 +103,14 @@ interface WorkflowEngineDeps {
|
|
|
100
103
|
stash: () => Promise<boolean>;
|
|
101
104
|
unstash: () => Promise<void>;
|
|
102
105
|
getDirtyFiles: () => Promise<string[]>;
|
|
106
|
+
pullDefaultBranch: (projectId: string, branch?: string) => Promise<void>;
|
|
107
|
+
pushToRemote: (projectId: string, branch?: string) => Promise<void>;
|
|
103
108
|
};
|
|
109
|
+
skillsDir: string;
|
|
104
110
|
bundleService?: BundleServiceDep;
|
|
105
111
|
ttsService?: TtsServiceDep;
|
|
106
112
|
videoRenderService?: VideoRenderServiceDep;
|
|
107
|
-
log: (tag: string,
|
|
113
|
+
log: (tag: string, ...args: any[]) => void;
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
// ── WorkflowEngine ────────────────────────────────────────────────────
|
|
@@ -114,20 +120,29 @@ export class WorkflowEngine {
|
|
|
114
120
|
private kanban: WorkflowEngineDeps['kanban'];
|
|
115
121
|
private claudeService: WorkflowEngineDeps['claudeService'];
|
|
116
122
|
private gitWorkflow: WorkflowEngineDeps['gitWorkflow'];
|
|
123
|
+
private skillsDir: string;
|
|
117
124
|
private bundleService?: BundleServiceDep;
|
|
118
125
|
private ttsService?: TtsServiceDep;
|
|
119
126
|
private videoRenderService?: VideoRenderServiceDep;
|
|
120
|
-
private log:
|
|
127
|
+
private log: (tag: string, ...args: any[]) => void;
|
|
128
|
+
private programmableExecutor: ProgrammableNodeExecutor;
|
|
121
129
|
|
|
122
|
-
|
|
130
|
+
/** Active Claude child processes keyed by executionId — for kill support */
|
|
131
|
+
private activeProcesses = new Map<string, ChildProcess>();
|
|
132
|
+
/** Execution IDs that were stopped by user — prevents executeGraph from overwriting statuses */
|
|
133
|
+
private stoppedExecutions = new Set<string>();
|
|
134
|
+
|
|
135
|
+
constructor({ db, kanban, claudeService, gitWorkflow, skillsDir, bundleService, ttsService, videoRenderService, log }: WorkflowEngineDeps) {
|
|
123
136
|
this.db = db;
|
|
124
137
|
this.kanban = kanban;
|
|
125
138
|
this.claudeService = claudeService;
|
|
126
139
|
this.gitWorkflow = gitWorkflow;
|
|
140
|
+
this.skillsDir = skillsDir;
|
|
127
141
|
this.bundleService = bundleService;
|
|
128
142
|
this.ttsService = ttsService;
|
|
129
143
|
this.videoRenderService = videoRenderService;
|
|
130
144
|
this.log = log;
|
|
145
|
+
this.programmableExecutor = new ProgrammableNodeExecutor({ log });
|
|
131
146
|
}
|
|
132
147
|
|
|
133
148
|
// ── Public API ──────────────────────────────────────────────────────
|
|
@@ -152,8 +167,15 @@ export class WorkflowEngine {
|
|
|
152
167
|
throw new Error(`Workflow ${workflowId} has no start node`);
|
|
153
168
|
}
|
|
154
169
|
|
|
170
|
+
// Pull latest default branch before creating worktree to ensure up-to-date base
|
|
171
|
+
try {
|
|
172
|
+
await this.gitWorkflow.pullDefaultBranch(projectId);
|
|
173
|
+
} catch (pullErr: any) {
|
|
174
|
+
this.log('WORKFLOW', `Pull before worktree creation failed (non-fatal):`, pullErr.stack || pullErr.message);
|
|
175
|
+
}
|
|
176
|
+
|
|
155
177
|
// Create worktree and feature branch
|
|
156
|
-
const worktreePath = await this.gitWorkflow.createWorktree(featureId);
|
|
178
|
+
const worktreePath = await this.gitWorkflow.createWorktree(featureId, projectId);
|
|
157
179
|
const branchName = `feature/${featureId}`;
|
|
158
180
|
|
|
159
181
|
// Build initial context
|
|
@@ -172,6 +194,7 @@ export class WorkflowEngine {
|
|
|
172
194
|
mainSkillDir,
|
|
173
195
|
mainToolsDir,
|
|
174
196
|
pidFlag,
|
|
197
|
+
nodeOutputs: {},
|
|
175
198
|
};
|
|
176
199
|
|
|
177
200
|
// Enrich context with feature data for agent prompt resolution
|
|
@@ -207,8 +230,8 @@ export class WorkflowEngine {
|
|
|
207
230
|
|
|
208
231
|
// Fire-and-forget: traverse the graph asynchronously
|
|
209
232
|
this.executeGraph(executionId, graph, startNode.id, context).catch(err => {
|
|
210
|
-
this.log('WORKFLOW', `UNCAUGHT ERROR in execution ${executionId}
|
|
211
|
-
this.setExecutionStatus(executionId, 'failed', null, context).catch(() => {});
|
|
233
|
+
this.log('WORKFLOW', `UNCAUGHT ERROR in execution ${executionId}:`, err.stack || err.message);
|
|
234
|
+
this.setExecutionStatus(executionId, 'failed', null, context).catch((e: any) => { this.log('WORKFLOW', 'Failed to set execution status:', e.message); });
|
|
212
235
|
});
|
|
213
236
|
|
|
214
237
|
return { executionId };
|
|
@@ -256,7 +279,7 @@ export class WorkflowEngine {
|
|
|
256
279
|
eq(workflowNodeExecutions.nodeId, currentNodeId),
|
|
257
280
|
));
|
|
258
281
|
|
|
259
|
-
const failedExec = nodeExecs.find(ne => ne.status === 'failed' || ne.status === 'running');
|
|
282
|
+
const failedExec = nodeExecs.find(ne => ne.status === 'failed' || ne.status === 'running' || ne.status === 'stopped');
|
|
260
283
|
if (failedExec) {
|
|
261
284
|
await this.db.update(workflowNodeExecutions)
|
|
262
285
|
.set({ status: 'pending', attempt: failedExec.attempt + 1 })
|
|
@@ -270,8 +293,8 @@ export class WorkflowEngine {
|
|
|
270
293
|
|
|
271
294
|
// Resume graph traversal from the current node
|
|
272
295
|
this.executeGraph(executionId, graph, currentNodeId, context).catch(err => {
|
|
273
|
-
this.log('WORKFLOW', `UNCAUGHT ERROR resuming ${executionId}
|
|
274
|
-
this.setExecutionStatus(executionId, 'failed', currentNodeId, context).catch(() => {});
|
|
296
|
+
this.log('WORKFLOW', `UNCAUGHT ERROR resuming ${executionId}:`, err.stack || err.message);
|
|
297
|
+
this.setExecutionStatus(executionId, 'failed', currentNodeId, context).catch((e: any) => { this.log('WORKFLOW', 'Failed to set execution status:', e.message); });
|
|
275
298
|
});
|
|
276
299
|
};
|
|
277
300
|
|
|
@@ -301,7 +324,7 @@ export class WorkflowEngine {
|
|
|
301
324
|
eq(workflowNodeExecutions.executionId, execution.id),
|
|
302
325
|
eq(workflowNodeExecutions.nodeId, nodeId),
|
|
303
326
|
));
|
|
304
|
-
const currentNodeExec = nodeExecs.find(ne => ne.status === 'running' || ne.status === 'failed');
|
|
327
|
+
const currentNodeExec = nodeExecs.find(ne => ne.status === 'running' || ne.status === 'failed' || ne.status === 'stopped');
|
|
305
328
|
if (currentNodeExec) {
|
|
306
329
|
await this.db.update(workflowNodeExecutions)
|
|
307
330
|
.set({ status: 'completed', completedAt: new Date().toISOString() })
|
|
@@ -326,8 +349,8 @@ export class WorkflowEngine {
|
|
|
326
349
|
|
|
327
350
|
// Resume graph traversal from the next node
|
|
328
351
|
this.executeGraph(execution.id, graph, nextNodeId, context).catch(err => {
|
|
329
|
-
this.log('WORKFLOW', `UNCAUGHT ERROR after force-next ${execution.id}
|
|
330
|
-
this.setExecutionStatus(execution.id, 'failed', nextNodeId, context).catch(() => {});
|
|
352
|
+
this.log('WORKFLOW', `UNCAUGHT ERROR after force-next ${execution.id}:`, err.stack || err.message);
|
|
353
|
+
this.setExecutionStatus(execution.id, 'failed', nextNodeId, context).catch((e: any) => { this.log('WORKFLOW', 'Failed to set execution status:', e.message); });
|
|
331
354
|
});
|
|
332
355
|
};
|
|
333
356
|
|
|
@@ -356,7 +379,7 @@ export class WorkflowEngine {
|
|
|
356
379
|
eq(workflowNodeExecutions.executionId, execution.id),
|
|
357
380
|
eq(workflowNodeExecutions.nodeId, nodeId),
|
|
358
381
|
));
|
|
359
|
-
const currentNodeExec = nodeExecs.find(ne => ne.status === 'running' || ne.status === 'failed');
|
|
382
|
+
const currentNodeExec = nodeExecs.find(ne => ne.status === 'running' || ne.status === 'failed' || ne.status === 'stopped');
|
|
360
383
|
if (currentNodeExec) {
|
|
361
384
|
await this.db.update(workflowNodeExecutions)
|
|
362
385
|
.set({ status: 'pending', attempt: currentNodeExec.attempt + 1 })
|
|
@@ -370,11 +393,268 @@ export class WorkflowEngine {
|
|
|
370
393
|
|
|
371
394
|
// Re-execute from the current node
|
|
372
395
|
this.executeGraph(execution.id, graph, nodeId, context).catch(err => {
|
|
373
|
-
this.log('WORKFLOW', `UNCAUGHT ERROR after restart-node ${execution.id}
|
|
374
|
-
this.setExecutionStatus(execution.id, 'failed', nodeId, context).catch(() => {});
|
|
396
|
+
this.log('WORKFLOW', `UNCAUGHT ERROR after restart-node ${execution.id}:`, err.stack || err.message);
|
|
397
|
+
this.setExecutionStatus(execution.id, 'failed', nodeId, context).catch((e: any) => { this.log('WORKFLOW', 'Failed to set execution status:', e.message); });
|
|
375
398
|
});
|
|
376
399
|
};
|
|
377
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Stop the currently executing workflow node.
|
|
403
|
+
* Sends SIGTERM to the Claude child process, waits 3s, then SIGKILL if still alive.
|
|
404
|
+
* Marks node execution as 'stopped' and workflow execution as 'failed'.
|
|
405
|
+
* Does NOT clean up the worktree (same as error behavior — preserves work for resume).
|
|
406
|
+
*/
|
|
407
|
+
stopNode = async (featureId: string, nodeId: string): Promise<void> => {
|
|
408
|
+
this.log('WORKFLOW', `Stop-node requested for feature ${featureId}, node ${nodeId}`);
|
|
409
|
+
|
|
410
|
+
const execution = await this.findLatestExecution(featureId);
|
|
411
|
+
if (!execution || execution.status !== 'running') {
|
|
412
|
+
throw new Error(`No running execution found for feature ${featureId}`);
|
|
413
|
+
}
|
|
414
|
+
if (execution.currentNodeId !== nodeId) {
|
|
415
|
+
throw new Error(`Node ${nodeId} is not the current execution node (current: ${execution.currentNodeId})`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Mark as stopped BEFORE killing to prevent executeGraph from overwriting statuses
|
|
419
|
+
this.stoppedExecutions.add(execution.id);
|
|
420
|
+
|
|
421
|
+
// Update node execution → 'stopped'
|
|
422
|
+
const nodeExecs = await this.db.select().from(workflowNodeExecutions)
|
|
423
|
+
.where(and(
|
|
424
|
+
eq(workflowNodeExecutions.executionId, execution.id),
|
|
425
|
+
eq(workflowNodeExecutions.nodeId, nodeId),
|
|
426
|
+
));
|
|
427
|
+
const runningExec = nodeExecs.find(ne => ne.status === 'running');
|
|
428
|
+
if (runningExec) {
|
|
429
|
+
await this.db.update(workflowNodeExecutions)
|
|
430
|
+
.set({
|
|
431
|
+
status: 'stopped',
|
|
432
|
+
error: 'Stopped by user',
|
|
433
|
+
completedAt: new Date().toISOString(),
|
|
434
|
+
})
|
|
435
|
+
.where(eq(workflowNodeExecutions.id, runningExec.id));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Update workflow execution → 'failed'
|
|
439
|
+
const context: ExecutionContext = JSON.parse(execution.context);
|
|
440
|
+
await this.setExecutionStatus(execution.id, 'failed', nodeId, context);
|
|
441
|
+
|
|
442
|
+
// Kill the child process
|
|
443
|
+
const child = this.activeProcesses.get(execution.id);
|
|
444
|
+
if (child) {
|
|
445
|
+
this.log('WORKFLOW', `Sending SIGTERM to child process for execution ${execution.id}`);
|
|
446
|
+
child.kill('SIGTERM');
|
|
447
|
+
// After 3s, send SIGKILL if still alive
|
|
448
|
+
setTimeout(() => {
|
|
449
|
+
try { child.kill('SIGKILL'); } catch { /* already dead */ }
|
|
450
|
+
}, 3000);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
this.log('WORKFLOW', `Node ${nodeId} stopped for feature ${featureId}`);
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Continue a stuck/failed agent node by resuming the Claude session.
|
|
458
|
+
* Spawns a new Claude process with '--resume <sessionId>' using the original session ID.
|
|
459
|
+
* If the process is still alive, kills it first before resuming.
|
|
460
|
+
*/
|
|
461
|
+
continueNode = async (featureId: string, nodeId: string): Promise<void> => {
|
|
462
|
+
this.log('WORKFLOW', `Continue-node requested for feature ${featureId}, node ${nodeId}`);
|
|
463
|
+
|
|
464
|
+
const execution = await this.findLatestExecution(featureId);
|
|
465
|
+
if (!execution) throw new Error(`No execution found for feature ${featureId}`);
|
|
466
|
+
if (execution.currentNodeId !== nodeId) {
|
|
467
|
+
throw new Error(`Node ${nodeId} is not the current execution node (current: ${execution.currentNodeId})`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Find the node execution record with the Claude session ID
|
|
471
|
+
const nodeExecs = await this.db.select().from(workflowNodeExecutions)
|
|
472
|
+
.where(and(
|
|
473
|
+
eq(workflowNodeExecutions.executionId, execution.id),
|
|
474
|
+
eq(workflowNodeExecutions.nodeId, nodeId),
|
|
475
|
+
));
|
|
476
|
+
const nodeExec = nodeExecs.sort((a, b) => b.attempt - a.attempt)[0];
|
|
477
|
+
if (!nodeExec) throw new Error(`No execution record found for node ${nodeId}`);
|
|
478
|
+
if (!nodeExec.claudeSessionId) {
|
|
479
|
+
throw new Error(`Node ${nodeId} has no Claude session ID — not an agent node`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// If the process is still alive, kill it first
|
|
483
|
+
const existingChild = this.activeProcesses.get(execution.id);
|
|
484
|
+
if (existingChild) {
|
|
485
|
+
this.log('WORKFLOW', `Killing existing process for execution ${execution.id} before resuming`);
|
|
486
|
+
this.stoppedExecutions.add(execution.id);
|
|
487
|
+
existingChild.kill('SIGTERM');
|
|
488
|
+
await new Promise<void>(resolve => {
|
|
489
|
+
const timeout = setTimeout(() => {
|
|
490
|
+
try { existingChild.kill('SIGKILL'); } catch { /* already dead */ }
|
|
491
|
+
resolve();
|
|
492
|
+
}, 3000);
|
|
493
|
+
existingChild.on('close', () => { clearTimeout(timeout); resolve(); });
|
|
494
|
+
});
|
|
495
|
+
this.activeProcesses.delete(execution.id);
|
|
496
|
+
this.stoppedExecutions.delete(execution.id);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Reset node execution status to 'running' — do NOT increment attempt (continuation, not retry)
|
|
500
|
+
await this.db.update(workflowNodeExecutions)
|
|
501
|
+
.set({
|
|
502
|
+
status: 'running',
|
|
503
|
+
error: null,
|
|
504
|
+
completedAt: null,
|
|
505
|
+
})
|
|
506
|
+
.where(eq(workflowNodeExecutions.id, nodeExec.id));
|
|
507
|
+
|
|
508
|
+
// Set workflow execution back to 'running'
|
|
509
|
+
const context: ExecutionContext = JSON.parse(execution.context);
|
|
510
|
+
await this.db.update(workflowExecutions)
|
|
511
|
+
.set({ status: 'running', updatedAt: new Date().toISOString() })
|
|
512
|
+
.where(eq(workflowExecutions.id, execution.id));
|
|
513
|
+
|
|
514
|
+
// Load workflow graph for post-continue traversal
|
|
515
|
+
const [workflow] = await this.db.select().from(workflows)
|
|
516
|
+
.where(eq(workflows.id, execution.workflowId));
|
|
517
|
+
if (!workflow) throw new Error(`Workflow ${execution.workflowId} not found`);
|
|
518
|
+
const graph: WorkflowGraph = JSON.parse(workflow.graphData);
|
|
519
|
+
|
|
520
|
+
// Resolve agent name for logging
|
|
521
|
+
const graphNode = graph.nodes.find(n => n.id === nodeId);
|
|
522
|
+
const agentId = (graphNode?.data as { agentId?: string })?.agentId;
|
|
523
|
+
let agentName = 'agent';
|
|
524
|
+
if (agentId) {
|
|
525
|
+
const [agent] = await this.db.select().from(agents).where(eq(agents.id, agentId));
|
|
526
|
+
if (agent) agentName = agent.name;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Set up the same streaming callbacks as handleRunAgent
|
|
530
|
+
const toolCalls: Record<string, number> = { total: 0, read: 0, write: 0, edit: 0, bash: 0, glob: 0, grep: 0 };
|
|
531
|
+
let costUsd: number | null = null;
|
|
532
|
+
let numTurns: number | null = null;
|
|
533
|
+
let stopReason: string | null = null;
|
|
534
|
+
let model: string | null = null;
|
|
535
|
+
let contextWindowPct: number | null = null;
|
|
536
|
+
|
|
537
|
+
const onToolUse = (toolName: string, toolInput: object) => {
|
|
538
|
+
toolCalls.total++;
|
|
539
|
+
const key = toolName.toLowerCase();
|
|
540
|
+
if (key in toolCalls) toolCalls[key]++;
|
|
541
|
+
|
|
542
|
+
// Append tool calls to existing records — no duplication
|
|
543
|
+
const target = this.extractToolTarget(toolName, toolInput);
|
|
544
|
+
this.db.insert(workflowToolCalls).values({
|
|
545
|
+
id: randomUUID(),
|
|
546
|
+
nodeExecutionId: nodeExec.id,
|
|
547
|
+
timestamp: new Date().toISOString(),
|
|
548
|
+
toolName,
|
|
549
|
+
target,
|
|
550
|
+
createdAt: new Date().toISOString(),
|
|
551
|
+
}).catch(() => {});
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const onAssistantText = (text: string) => {
|
|
555
|
+
this.db.insert(workflowToolCalls).values({
|
|
556
|
+
id: randomUUID(),
|
|
557
|
+
nodeExecutionId: nodeExec.id,
|
|
558
|
+
timestamp: new Date().toISOString(),
|
|
559
|
+
toolName: '__assistant__',
|
|
560
|
+
target: text,
|
|
561
|
+
createdAt: new Date().toISOString(),
|
|
562
|
+
}).catch(() => {});
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const onResult = (metadata: {
|
|
566
|
+
costUsd: number | null;
|
|
567
|
+
numTurns: number | null;
|
|
568
|
+
stopReason: string | null;
|
|
569
|
+
model: string | null;
|
|
570
|
+
contextWindow: number | null;
|
|
571
|
+
}) => {
|
|
572
|
+
costUsd = metadata.costUsd;
|
|
573
|
+
numTurns = metadata.numTurns;
|
|
574
|
+
stopReason = metadata.stopReason;
|
|
575
|
+
model = metadata.model;
|
|
576
|
+
contextWindowPct = metadata.contextWindow;
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const onSpawn = (child: ChildProcess) => {
|
|
580
|
+
this.activeProcesses.set(execution.id, child);
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// Fire-and-forget: resume the Claude session and continue graph traversal
|
|
584
|
+
const { worktreePath } = context;
|
|
585
|
+
const label = `${agentName.toLowerCase().replace(/\s+/g, '-')}:${featureId}`;
|
|
586
|
+
|
|
587
|
+
this.log('WORKFLOW', `Resuming Claude session ${nodeExec.claudeSessionId} for agent "${agentName}"`);
|
|
588
|
+
|
|
589
|
+
(async () => {
|
|
590
|
+
try {
|
|
591
|
+
const output = await this.claudeService.spawnClaude(
|
|
592
|
+
'Continue',
|
|
593
|
+
worktreePath,
|
|
594
|
+
label,
|
|
595
|
+
{ onToolUse, onResult, onAssistantText, onSpawn, resume: nodeExec.claudeSessionId },
|
|
596
|
+
);
|
|
597
|
+
this.activeProcesses.delete(execution.id);
|
|
598
|
+
|
|
599
|
+
// If execution was stopped while continuing, exit
|
|
600
|
+
if (this.stoppedExecutions.has(execution.id)) {
|
|
601
|
+
this.stoppedExecutions.delete(execution.id);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Commit any changes
|
|
606
|
+
try {
|
|
607
|
+
await this.gitWorkflow.commitChanges(featureId, worktreePath);
|
|
608
|
+
} catch (err: any) {
|
|
609
|
+
this.log('WORKFLOW', `WARN: Auto-commit after continue failed: ${err.message}`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Rebase onto main
|
|
613
|
+
try {
|
|
614
|
+
await this.gitWorkflow.rebaseBranch(featureId, worktreePath);
|
|
615
|
+
} catch (err: any) {
|
|
616
|
+
this.log('WORKFLOW', `WARN: Rebase after continue failed: ${err.message}`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Mark node as completed
|
|
620
|
+
await this.completeNodeExecution(nodeExec.id, {
|
|
621
|
+
agentId,
|
|
622
|
+
agentName,
|
|
623
|
+
output,
|
|
624
|
+
toolCalls,
|
|
625
|
+
costUsd,
|
|
626
|
+
numTurns,
|
|
627
|
+
stopReason,
|
|
628
|
+
model,
|
|
629
|
+
contextWindowPct,
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
this.log('WORKFLOW', `Continued agent "${agentName}" completed — resuming graph traversal`);
|
|
633
|
+
|
|
634
|
+
// Continue graph traversal from next node
|
|
635
|
+
const nextNodeId = this.resolveNextNode(graph, nodeId, null);
|
|
636
|
+
if (nextNodeId) {
|
|
637
|
+
await this.executeGraph(execution.id, graph, nextNodeId, context);
|
|
638
|
+
} else {
|
|
639
|
+
await this.setExecutionStatus(execution.id, 'completed', null, context);
|
|
640
|
+
}
|
|
641
|
+
} catch (err: any) {
|
|
642
|
+
this.activeProcesses.delete(execution.id);
|
|
643
|
+
|
|
644
|
+
if (this.stoppedExecutions.has(execution.id)) {
|
|
645
|
+
this.stoppedExecutions.delete(execution.id);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
this.log('WORKFLOW', `Continue-node FAILED: ${err.message}`);
|
|
650
|
+
await this.db.update(workflowNodeExecutions)
|
|
651
|
+
.set({ status: 'failed', error: err.message, completedAt: new Date().toISOString() })
|
|
652
|
+
.where(eq(workflowNodeExecutions.id, nodeExec.id));
|
|
653
|
+
await this.setExecutionStatus(execution.id, 'failed', nodeId, context);
|
|
654
|
+
}
|
|
655
|
+
})();
|
|
656
|
+
};
|
|
657
|
+
|
|
378
658
|
/**
|
|
379
659
|
* Find the most recent execution for a feature (any status).
|
|
380
660
|
* Returns the execution row or null.
|
|
@@ -411,7 +691,7 @@ export class WorkflowEngine {
|
|
|
411
691
|
.where(eq(workflowNodeExecutions.executionId, execution.id));
|
|
412
692
|
|
|
413
693
|
const completed = nodeExecs.filter(ne => ne.status === 'completed').map(ne => ne.nodeId);
|
|
414
|
-
const failed = nodeExecs.filter(ne => ne.status === 'failed').map(ne => ne.nodeId);
|
|
694
|
+
const failed = nodeExecs.filter(ne => ne.status === 'failed' || ne.status === 'stopped').map(ne => ne.nodeId);
|
|
415
695
|
const pending = nodeExecs.filter(ne => ne.status === 'pending' || ne.status === 'running').map(ne => ne.nodeId);
|
|
416
696
|
|
|
417
697
|
// Resolve current node type and agent name from workflow graph
|
|
@@ -436,13 +716,13 @@ export class WorkflowEngine {
|
|
|
436
716
|
}
|
|
437
717
|
}
|
|
438
718
|
}
|
|
439
|
-
} catch {
|
|
719
|
+
} catch (err: any) { this.log('WORKFLOW', 'Non-critical error:', err.message); }
|
|
440
720
|
}
|
|
441
721
|
|
|
442
|
-
// Get error message from the most recent failed node execution
|
|
722
|
+
// Get error message from the most recent failed/stopped node execution
|
|
443
723
|
let errorMessage: string | null = null;
|
|
444
724
|
if (execution.status === 'failed') {
|
|
445
|
-
const failedExec = nodeExecs.find(ne => ne.status === 'failed' && ne.error);
|
|
725
|
+
const failedExec = nodeExecs.find(ne => (ne.status === 'failed' || ne.status === 'stopped') && ne.error);
|
|
446
726
|
if (failedExec) errorMessage = failedExec.error;
|
|
447
727
|
}
|
|
448
728
|
|
|
@@ -555,7 +835,7 @@ export class WorkflowEngine {
|
|
|
555
835
|
this.log('WORKFLOW', `Executing node ${node.id} (${node.type})`);
|
|
556
836
|
|
|
557
837
|
// Create or reset node execution record
|
|
558
|
-
const nodeExecId = await this.upsertNodeExecution(executionId, node.id, 'running');
|
|
838
|
+
const nodeExecId = await this.upsertNodeExecution(executionId, node.id, 'running', context.cycle || 1);
|
|
559
839
|
|
|
560
840
|
try {
|
|
561
841
|
const result = await this.executeNode(node, context, executionId);
|
|
@@ -564,6 +844,14 @@ export class WorkflowEngine {
|
|
|
564
844
|
await this.completeNodeExecution(nodeExecId, result.outputData || null);
|
|
565
845
|
this.log('WORKFLOW', `Node ${node.id} completed (output: ${result.outputLabel || 'default'})`);
|
|
566
846
|
|
|
847
|
+
// Store output in context.nodeOutputs under the node's label (dec_2266dcd3)
|
|
848
|
+
if (result.outputData) {
|
|
849
|
+
const nodeOutputs = (context.nodeOutputs || {}) as Record<string, unknown>;
|
|
850
|
+
const nodeLabel = (node.data?.label as string) || node.id;
|
|
851
|
+
nodeOutputs[nodeLabel] = result.outputData;
|
|
852
|
+
context.nodeOutputs = nodeOutputs;
|
|
853
|
+
}
|
|
854
|
+
|
|
567
855
|
// Find the next node via edges
|
|
568
856
|
currentNodeId = this.resolveNextNode(graph, node.id, result.outputLabel);
|
|
569
857
|
|
|
@@ -573,7 +861,14 @@ export class WorkflowEngine {
|
|
|
573
861
|
this.log('WORKFLOW', `No outgoing edge from ${node.id} — traversal complete`);
|
|
574
862
|
}
|
|
575
863
|
} catch (err: any) {
|
|
576
|
-
this
|
|
864
|
+
// If this execution was stopped by user, statuses are already set — just exit
|
|
865
|
+
if (this.stoppedExecutions.has(executionId)) {
|
|
866
|
+
this.stoppedExecutions.delete(executionId);
|
|
867
|
+
this.log('WORKFLOW', `Node ${node.id} stopped by user — exiting graph traversal`);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
this.log('WORKFLOW', `Node ${node.id} FAILED:`, err.stack || err.message);
|
|
577
872
|
|
|
578
873
|
// Mark node execution as failed
|
|
579
874
|
await this.db.update(workflowNodeExecutions)
|
|
@@ -629,6 +924,8 @@ export class WorkflowEngine {
|
|
|
629
924
|
return this.handleGenerateTTS(node, context);
|
|
630
925
|
case 'renderVideo':
|
|
631
926
|
return this.handleRenderVideo(node, context);
|
|
927
|
+
case 'programmable':
|
|
928
|
+
return this.handleProgrammable(node, context);
|
|
632
929
|
default:
|
|
633
930
|
throw new Error(`Unknown node type: ${node.type}`);
|
|
634
931
|
}
|
|
@@ -687,8 +984,23 @@ export class WorkflowEngine {
|
|
|
687
984
|
throw new Error(`Agent ${agentId} not found`);
|
|
688
985
|
}
|
|
689
986
|
|
|
690
|
-
//
|
|
691
|
-
const
|
|
987
|
+
// Load skill file contents from disk (never interpolated)
|
|
988
|
+
const skillIdentifiers: string[] = JSON.parse(agent.skills);
|
|
989
|
+
const skillContents: string[] = [];
|
|
990
|
+
for (const skillId of skillIdentifiers) {
|
|
991
|
+
const skillPath = pathJoin(this.skillsDir, skillId, 'SKILL.md');
|
|
992
|
+
try {
|
|
993
|
+
skillContents.push(readFileSync(skillPath, 'utf-8'));
|
|
994
|
+
} catch (err: any) {
|
|
995
|
+
throw new Error(`Failed to read skill file for "${skillId}" at ${skillPath}: ${err.message}`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Resolve {{placeholder}} variables only in the grounding portion
|
|
1000
|
+
const resolvedGrounding = this.resolvePromptTemplate(agent.grounding, context);
|
|
1001
|
+
|
|
1002
|
+
// Compose final system prompt: skill contents + resolved grounding
|
|
1003
|
+
const resolvedPrompt = [...skillContents, resolvedGrounding].join('\n\n');
|
|
692
1004
|
|
|
693
1005
|
// Find the node execution ID for persisting tool calls
|
|
694
1006
|
const nodeExecs = await this.db.select().from(workflowNodeExecutions)
|
|
@@ -696,9 +1008,18 @@ export class WorkflowEngine {
|
|
|
696
1008
|
eq(workflowNodeExecutions.executionId, executionId),
|
|
697
1009
|
eq(workflowNodeExecutions.nodeId, node.id),
|
|
698
1010
|
));
|
|
699
|
-
const
|
|
700
|
-
? nodeExecs.sort((a, b) => b.attempt - a.attempt)[0]
|
|
1011
|
+
const currentNodeExec = nodeExecs.length > 0
|
|
1012
|
+
? nodeExecs.sort((a, b) => b.attempt - a.attempt)[0]
|
|
701
1013
|
: null;
|
|
1014
|
+
const currentNodeExecId = currentNodeExec?.id ?? null;
|
|
1015
|
+
|
|
1016
|
+
// Generate a Claude session ID and store it on the node execution record
|
|
1017
|
+
const claudeSessionId = randomUUID();
|
|
1018
|
+
if (currentNodeExecId) {
|
|
1019
|
+
await this.db.update(workflowNodeExecutions)
|
|
1020
|
+
.set({ claudeSessionId })
|
|
1021
|
+
.where(eq(workflowNodeExecutions.id, currentNodeExecId));
|
|
1022
|
+
}
|
|
702
1023
|
|
|
703
1024
|
// Accumulate KPI metrics during execution
|
|
704
1025
|
const toolCalls: Record<string, number> = { total: 0, read: 0, write: 0, edit: 0, bash: 0, glob: 0, grep: 0 };
|
|
@@ -757,26 +1078,36 @@ export class WorkflowEngine {
|
|
|
757
1078
|
contextWindowPct = metadata.contextWindow;
|
|
758
1079
|
};
|
|
759
1080
|
|
|
1081
|
+
// Track child process for kill support
|
|
1082
|
+
const onSpawn = (child: ChildProcess) => {
|
|
1083
|
+
this.activeProcesses.set(executionId, child);
|
|
1084
|
+
};
|
|
1085
|
+
|
|
760
1086
|
this.log('WORKFLOW', `Running agent "${agent.name}" (${agentId}) for ${featureId}`);
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1087
|
+
let output: string;
|
|
1088
|
+
try {
|
|
1089
|
+
output = await this.claudeService.spawnClaude(
|
|
1090
|
+
resolvedPrompt,
|
|
1091
|
+
worktreePath,
|
|
1092
|
+
`${agent.name.toLowerCase().replace(/\s+/g, '-')}:${featureId}`,
|
|
1093
|
+
{ onToolUse, onResult, onAssistantText, onSpawn, sessionId: claudeSessionId, model: agent.model },
|
|
1094
|
+
);
|
|
1095
|
+
} finally {
|
|
1096
|
+
this.activeProcesses.delete(executionId);
|
|
1097
|
+
}
|
|
767
1098
|
|
|
768
1099
|
// Commit any changes the agent made
|
|
769
1100
|
try {
|
|
770
1101
|
await this.gitWorkflow.commitChanges(featureId, worktreePath);
|
|
771
1102
|
} catch (err: any) {
|
|
772
|
-
this.log('WORKFLOW', `WARN: Auto-commit after agent failed
|
|
1103
|
+
this.log('WORKFLOW', `WARN: Auto-commit after agent failed:`, err.stack || err.message);
|
|
773
1104
|
}
|
|
774
1105
|
|
|
775
1106
|
// Rebase onto main
|
|
776
1107
|
try {
|
|
777
1108
|
await this.gitWorkflow.rebaseBranch(featureId, worktreePath);
|
|
778
1109
|
} catch (err: any) {
|
|
779
|
-
this.log('WORKFLOW', `WARN: Rebase after agent failed
|
|
1110
|
+
this.log('WORKFLOW', `WARN: Rebase after agent failed:`, err.stack || err.message);
|
|
780
1111
|
}
|
|
781
1112
|
|
|
782
1113
|
this.log('WORKFLOW', `Agent "${agent.name}" completed (${output.length} chars output)`);
|
|
@@ -895,11 +1226,11 @@ export class WorkflowEngine {
|
|
|
895
1226
|
await this.handleMerge(featureId, worktreePath, context);
|
|
896
1227
|
await this.setExecutionStatus(executionId, 'completed', node.id, context);
|
|
897
1228
|
} else if (outcome === 'blocked') {
|
|
898
|
-
await this.cleanupWorktree(featureId, worktreePath);
|
|
1229
|
+
await this.cleanupWorktree(featureId, worktreePath, context.projectId);
|
|
899
1230
|
await this.setExecutionStatus(executionId, 'completed', node.id, context);
|
|
900
1231
|
} else {
|
|
901
1232
|
// failed or unknown outcome
|
|
902
|
-
await this.cleanupWorktree(featureId, worktreePath);
|
|
1233
|
+
await this.cleanupWorktree(featureId, worktreePath, context.projectId);
|
|
903
1234
|
await this.setExecutionStatus(executionId, 'failed', node.id, context);
|
|
904
1235
|
}
|
|
905
1236
|
|
|
@@ -1075,6 +1406,67 @@ export class WorkflowEngine {
|
|
|
1075
1406
|
};
|
|
1076
1407
|
};
|
|
1077
1408
|
|
|
1409
|
+
/**
|
|
1410
|
+
* Programmable — executes user-written JavaScript in an isolated-vm V8 isolate.
|
|
1411
|
+
* Returns { outputLabel: 'success', outputData } on success,
|
|
1412
|
+
* or { outputLabel: 'error', outputData: { error } } on throw.
|
|
1413
|
+
*
|
|
1414
|
+
* The error output label mechanism:
|
|
1415
|
+
* - If the 'error' output handle is wired → workflow continues on the error path.
|
|
1416
|
+
* - If the 'error' output handle is NOT wired → resolveNextNode throws → workflow fails.
|
|
1417
|
+
*/
|
|
1418
|
+
private handleProgrammable = async (
|
|
1419
|
+
node: WorkflowNode,
|
|
1420
|
+
context: ExecutionContext,
|
|
1421
|
+
): Promise<NodeHandlerResult> => {
|
|
1422
|
+
const { code, label, timeout, memory, maxRequests, maxResponseSize } = node.data as {
|
|
1423
|
+
code: string;
|
|
1424
|
+
label: string;
|
|
1425
|
+
timeout?: number;
|
|
1426
|
+
memory?: number;
|
|
1427
|
+
maxRequests?: number;
|
|
1428
|
+
maxResponseSize?: number;
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
if (!code || !code.trim()) {
|
|
1432
|
+
throw new Error(`Programmable node "${label || node.id}" has no code`);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const nodeLabel = label || node.id;
|
|
1436
|
+
this.log('WORKFLOW', `Running programmable node "${nodeLabel}"`);
|
|
1437
|
+
|
|
1438
|
+
// Build the context object that the user's run(context) receives.
|
|
1439
|
+
// Include nodeOutputs from previous nodes for data piping.
|
|
1440
|
+
const sandboxContext: Record<string, unknown> = {
|
|
1441
|
+
featureId: context.featureId,
|
|
1442
|
+
projectId: context.projectId,
|
|
1443
|
+
cycle: context.cycle,
|
|
1444
|
+
nodeOutputs: context.nodeOutputs || {},
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
const executionResult = await this.programmableExecutor.execute(code, sandboxContext, {
|
|
1448
|
+
timeout,
|
|
1449
|
+
memory,
|
|
1450
|
+
maxRequests,
|
|
1451
|
+
maxResponseSize,
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
if (executionResult.success) {
|
|
1455
|
+
this.log('WORKFLOW', `Programmable node "${nodeLabel}" succeeded`);
|
|
1456
|
+
return {
|
|
1457
|
+
outputLabel: 'success',
|
|
1458
|
+
outputData: executionResult.result as Record<string, unknown> ?? {},
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// User code threw — follow the error output handle
|
|
1463
|
+
this.log('WORKFLOW', `Programmable node "${nodeLabel}" errored: ${executionResult.error}`);
|
|
1464
|
+
return {
|
|
1465
|
+
outputLabel: 'error',
|
|
1466
|
+
outputData: { error: executionResult.error },
|
|
1467
|
+
};
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1078
1470
|
// ── Merge & Cleanup Helpers ─────────────────────────────────────────
|
|
1079
1471
|
|
|
1080
1472
|
/**
|
|
@@ -1090,20 +1482,20 @@ export class WorkflowEngine {
|
|
|
1090
1482
|
// Stash local changes on the parent branch
|
|
1091
1483
|
let stashed = false;
|
|
1092
1484
|
try {
|
|
1093
|
-
stashed = await this.gitWorkflow.stash();
|
|
1485
|
+
stashed = await this.gitWorkflow.stash(context.projectId);
|
|
1094
1486
|
} catch (err: any) {
|
|
1095
|
-
this.log('WORKFLOW', `Stash failed (non-fatal)
|
|
1487
|
+
this.log('WORKFLOW', `Stash failed (non-fatal):`, err.stack || err.message);
|
|
1096
1488
|
}
|
|
1097
1489
|
|
|
1098
1490
|
// Merge
|
|
1099
1491
|
let mergeSucceeded = false;
|
|
1100
1492
|
try {
|
|
1101
|
-
await this.gitWorkflow.mergeBranch(featureId);
|
|
1493
|
+
await this.gitWorkflow.mergeBranch(featureId, context.projectId);
|
|
1102
1494
|
mergeSucceeded = true;
|
|
1103
1495
|
} catch (mergeErr: any) {
|
|
1104
|
-
this.log('WORKFLOW', `Merge FAILED
|
|
1496
|
+
this.log('WORKFLOW', `Merge FAILED:`, mergeErr.stack || mergeErr.message, '— attempting fix');
|
|
1105
1497
|
try {
|
|
1106
|
-
const fixed = await this.gitWorkflow.fixMergeConflicts(featureId);
|
|
1498
|
+
const fixed = await this.gitWorkflow.fixMergeConflicts(featureId, context.projectId);
|
|
1107
1499
|
if (fixed) {
|
|
1108
1500
|
this.log('WORKFLOW', `Merge fix succeeded`);
|
|
1109
1501
|
mergeSucceeded = true;
|
|
@@ -1111,7 +1503,7 @@ export class WorkflowEngine {
|
|
|
1111
1503
|
throw new Error(`Merge fix did not result in a successful merge`);
|
|
1112
1504
|
}
|
|
1113
1505
|
} catch (fixErr: any) {
|
|
1114
|
-
this.log('WORKFLOW', `Merge fix FAILED
|
|
1506
|
+
this.log('WORKFLOW', `Merge fix FAILED:`, fixErr.stack || fixErr.message);
|
|
1115
1507
|
throw new Error(`Merge failed: ${mergeErr.message}`);
|
|
1116
1508
|
}
|
|
1117
1509
|
}
|
|
@@ -1119,16 +1511,26 @@ export class WorkflowEngine {
|
|
|
1119
1511
|
// Pop stash
|
|
1120
1512
|
if (stashed) {
|
|
1121
1513
|
try {
|
|
1122
|
-
await this.gitWorkflow.unstash();
|
|
1514
|
+
await this.gitWorkflow.unstash(context.projectId);
|
|
1123
1515
|
} catch (popErr: any) {
|
|
1124
|
-
this.log('WORKFLOW', `Stash pop failed
|
|
1516
|
+
this.log('WORKFLOW', `Stash pop failed:`, popErr.stack || popErr.message);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Push to remote after successful merge (non-fatal — merge already succeeded locally)
|
|
1521
|
+
if (mergeSucceeded) {
|
|
1522
|
+
try {
|
|
1523
|
+
await this.gitWorkflow.pushToRemote(context.projectId);
|
|
1524
|
+
this.log('WORKFLOW', `Push to remote succeeded for ${featureId}`);
|
|
1525
|
+
} catch (pushErr: any) {
|
|
1526
|
+
this.log('WORKFLOW', `WARN: Push to remote failed (non-fatal, merge succeeded locally):`, pushErr.stack || pushErr.message);
|
|
1125
1527
|
}
|
|
1126
1528
|
}
|
|
1127
1529
|
|
|
1128
1530
|
// Cleanup worktree + branch
|
|
1129
1531
|
if (mergeSucceeded) {
|
|
1130
|
-
await this.gitWorkflow.removeWorktree(worktreePath);
|
|
1131
|
-
await this.gitWorkflow.deleteBranch(featureId);
|
|
1532
|
+
await this.gitWorkflow.removeWorktree(worktreePath, context.projectId);
|
|
1533
|
+
await this.gitWorkflow.deleteBranch(featureId, context.projectId);
|
|
1132
1534
|
this.log('WORKFLOW', `Merge completed, worktree and branch cleaned up for ${featureId}`);
|
|
1133
1535
|
}
|
|
1134
1536
|
};
|
|
@@ -1136,10 +1538,10 @@ export class WorkflowEngine {
|
|
|
1136
1538
|
/**
|
|
1137
1539
|
* Remove the worktree and branch without merging.
|
|
1138
1540
|
*/
|
|
1139
|
-
private cleanupWorktree = async (featureId: string, worktreePath: string): Promise<void> => {
|
|
1541
|
+
private cleanupWorktree = async (featureId: string, worktreePath: string, projectId?: string): Promise<void> => {
|
|
1140
1542
|
this.log('WORKFLOW', `Cleaning up worktree for ${featureId} (no merge)`);
|
|
1141
|
-
await this.gitWorkflow.removeWorktree(worktreePath);
|
|
1142
|
-
await this.gitWorkflow.deleteBranch(featureId, true);
|
|
1543
|
+
await this.gitWorkflow.removeWorktree(worktreePath, projectId!);
|
|
1544
|
+
await this.gitWorkflow.deleteBranch(featureId, projectId!, true);
|
|
1143
1545
|
};
|
|
1144
1546
|
|
|
1145
1547
|
// ── Graph Helpers ───────────────────────────────────────────────────
|
|
@@ -1297,10 +1699,10 @@ export class WorkflowEngine {
|
|
|
1297
1699
|
// Parse graph data
|
|
1298
1700
|
const graphData = JSON.parse(workflow.graphData);
|
|
1299
1701
|
|
|
1300
|
-
// Build node execution map
|
|
1301
|
-
const nodeExecutionMap: Record<string, Record<string, unknown
|
|
1702
|
+
// Build node execution map — array per node, sorted by cycle
|
|
1703
|
+
const nodeExecutionMap: Record<string, Array<Record<string, unknown>>> = {};
|
|
1302
1704
|
for (const ne of nodeExecs) {
|
|
1303
|
-
|
|
1705
|
+
const entry = {
|
|
1304
1706
|
id: ne.id,
|
|
1305
1707
|
nodeId: ne.nodeId,
|
|
1306
1708
|
status: ne.status,
|
|
@@ -1309,8 +1711,17 @@ export class WorkflowEngine {
|
|
|
1309
1711
|
outputData: ne.outputData ? JSON.parse(ne.outputData) : null,
|
|
1310
1712
|
error: ne.error,
|
|
1311
1713
|
attempt: ne.attempt,
|
|
1714
|
+
cycle: ne.cycle,
|
|
1312
1715
|
toolCalls: toolCallsByNodeExec[ne.id] || [],
|
|
1716
|
+
claudeSessionId: ne.claudeSessionId || null,
|
|
1717
|
+
processAlive: this.activeProcesses.has(execution.id) && ne.status === 'running',
|
|
1313
1718
|
};
|
|
1719
|
+
if (!nodeExecutionMap[ne.nodeId]) nodeExecutionMap[ne.nodeId] = [];
|
|
1720
|
+
nodeExecutionMap[ne.nodeId].push(entry);
|
|
1721
|
+
}
|
|
1722
|
+
// Sort each node's executions by cycle ascending
|
|
1723
|
+
for (const nodeId of Object.keys(nodeExecutionMap)) {
|
|
1724
|
+
nodeExecutionMap[nodeId].sort((a, b) => (a.cycle as number) - (b.cycle as number));
|
|
1314
1725
|
}
|
|
1315
1726
|
|
|
1316
1727
|
return {
|
|
@@ -1367,12 +1778,14 @@ export class WorkflowEngine {
|
|
|
1367
1778
|
|
|
1368
1779
|
/**
|
|
1369
1780
|
* Create or reset a node execution record. Returns the record ID.
|
|
1370
|
-
* For
|
|
1781
|
+
* For same-cycle re-execution (restart), updates existing record.
|
|
1782
|
+
* For new cycle, creates a new row preserving previous cycle data.
|
|
1371
1783
|
*/
|
|
1372
1784
|
private upsertNodeExecution = async (
|
|
1373
1785
|
executionId: string,
|
|
1374
1786
|
nodeId: string,
|
|
1375
1787
|
status: string,
|
|
1788
|
+
cycle: number = 1,
|
|
1376
1789
|
): Promise<string> => {
|
|
1377
1790
|
// Check for existing records for this node in this execution
|
|
1378
1791
|
const existing = await this.db.select().from(workflowNodeExecutions)
|
|
@@ -1384,22 +1797,42 @@ export class WorkflowEngine {
|
|
|
1384
1797
|
const now = new Date().toISOString();
|
|
1385
1798
|
|
|
1386
1799
|
if (existing.length > 0) {
|
|
1387
|
-
//
|
|
1388
|
-
const
|
|
1389
|
-
|
|
1800
|
+
// Check if there's already a record for this cycle
|
|
1801
|
+
const sameCycle = existing.find(e => e.cycle === cycle);
|
|
1802
|
+
if (sameCycle) {
|
|
1803
|
+
// Same cycle re-execution (restart) — update existing record
|
|
1804
|
+
const newAttempt = sameCycle.status === 'completed' ? sameCycle.attempt + 1 : sameCycle.attempt;
|
|
1390
1805
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1806
|
+
await this.db.update(workflowNodeExecutions)
|
|
1807
|
+
.set({
|
|
1808
|
+
status,
|
|
1809
|
+
startedAt: now,
|
|
1810
|
+
completedAt: null,
|
|
1811
|
+
outputData: null,
|
|
1812
|
+
error: null,
|
|
1813
|
+
attempt: newAttempt,
|
|
1814
|
+
})
|
|
1815
|
+
.where(eq(workflowNodeExecutions.id, sameCycle.id));
|
|
1816
|
+
|
|
1817
|
+
return sameCycle.id;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// New cycle — create a new row to preserve previous cycle data
|
|
1821
|
+
const id = randomUUID();
|
|
1822
|
+
await this.db.insert(workflowNodeExecutions).values({
|
|
1823
|
+
id,
|
|
1824
|
+
executionId,
|
|
1825
|
+
nodeId,
|
|
1826
|
+
status,
|
|
1827
|
+
startedAt: now,
|
|
1828
|
+
completedAt: null,
|
|
1829
|
+
outputData: null,
|
|
1830
|
+
error: null,
|
|
1831
|
+
attempt: 1,
|
|
1832
|
+
cycle,
|
|
1833
|
+
});
|
|
1401
1834
|
|
|
1402
|
-
return
|
|
1835
|
+
return id;
|
|
1403
1836
|
}
|
|
1404
1837
|
|
|
1405
1838
|
// First execution of this node
|
|
@@ -1414,6 +1847,7 @@ export class WorkflowEngine {
|
|
|
1414
1847
|
outputData: null,
|
|
1415
1848
|
error: null,
|
|
1416
1849
|
attempt: 1,
|
|
1850
|
+
cycle,
|
|
1417
1851
|
});
|
|
1418
1852
|
|
|
1419
1853
|
return id;
|