@assistkick/create 1.10.0 → 1.12.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.
Files changed (209) hide show
  1. package/dist/src/scaffolder.d.ts +12 -1
  2. package/dist/src/scaffolder.js +40 -3
  3. package/dist/src/scaffolder.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/assistkick-product-system/package.json +1 -1
  6. package/templates/assistkick-product-system/packages/backend/package.json +1 -0
  7. package/templates/assistkick-product-system/packages/backend/src/mcp/permission_mcp_server.ts +196 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +31 -7
  9. package/templates/assistkick-product-system/packages/backend/src/routes/auth.ts +15 -12
  10. package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.test.ts +95 -0
  11. package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.ts +97 -0
  12. package/templates/assistkick-product-system/packages/backend/src/routes/chat_permission.ts +94 -0
  13. package/templates/assistkick-product-system/packages/backend/src/routes/chat_sessions.ts +189 -0
  14. package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.test.ts +131 -0
  15. package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.ts +94 -0
  16. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +12 -3
  17. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +2 -2
  18. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +390 -22
  19. package/templates/assistkick-product-system/packages/backend/src/routes/git_branches.test.ts +306 -0
  20. package/templates/assistkick-product-system/packages/backend/src/routes/git_connect.test.ts +133 -0
  21. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +66 -9
  22. package/templates/assistkick-product-system/packages/backend/src/routes/preview.ts +204 -0
  23. package/templates/assistkick-product-system/packages/backend/src/routes/projects.test.ts +205 -0
  24. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +37 -9
  25. package/templates/assistkick-product-system/packages/backend/src/routes/skills.test.ts +139 -0
  26. package/templates/assistkick-product-system/packages/backend/src/routes/skills.ts +95 -0
  27. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +5 -4
  28. package/templates/assistkick-product-system/packages/backend/src/routes/users.ts +4 -4
  29. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +8 -8
  30. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +5 -5
  31. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +6 -6
  32. package/templates/assistkick-product-system/packages/backend/src/server.ts +107 -27
  33. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +105 -203
  34. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +76 -266
  35. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.test.ts +427 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +345 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.test.ts +170 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.ts +106 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.test.ts +217 -0
  40. package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.ts +188 -0
  41. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.test.ts +1243 -0
  42. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +894 -0
  43. package/templates/assistkick-product-system/packages/backend/src/services/coherence-review.ts +3 -3
  44. package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.test.ts +85 -0
  45. package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.ts +54 -0
  46. package/templates/assistkick-product-system/packages/backend/src/services/email_service.ts +13 -10
  47. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +11 -3
  48. package/templates/assistkick-product-system/packages/backend/src/services/invitation_service.ts +1 -1
  49. package/templates/assistkick-product-system/packages/backend/src/services/password_reset_service.ts +1 -1
  50. package/templates/assistkick-product-system/packages/backend/src/services/permission_service.test.ts +243 -0
  51. package/templates/assistkick-product-system/packages/backend/src/services/permission_service.ts +259 -0
  52. package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.test.ts +172 -0
  53. package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.ts +225 -0
  54. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +29 -0
  55. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +17 -0
  56. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +255 -0
  57. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +300 -25
  58. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +44 -0
  59. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +62 -7
  60. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +77 -6
  61. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +129 -8
  62. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +2 -1
  63. package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.test.ts +45 -0
  64. package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.ts +157 -0
  65. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +4 -3
  66. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +3 -3
  67. package/templates/assistkick-product-system/packages/frontend/package.json +5 -0
  68. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +2 -0
  69. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +336 -5
  70. package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +192 -12
  71. package/templates/assistkick-product-system/packages/frontend/src/components/AttachmentPreviewList.tsx +98 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/AutocompleteDropdown.tsx +65 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ChatAttachButton.tsx +56 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ChatDropZone.tsx +80 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx +155 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx +182 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageInput.tsx +233 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ChatSessionSidebar.tsx +218 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ChatStopButton.tsx +32 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ChatTodoSidebar.tsx +113 -0
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +842 -0
  82. package/templates/assistkick-product-system/packages/frontend/src/components/CommitMessageModal.tsx +82 -0
  83. package/templates/assistkick-product-system/packages/frontend/src/components/DiagramOverlay.tsx +160 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +5 -5
  85. package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +9 -10
  86. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +5 -5
  87. package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +112 -41
  88. package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +2 -2
  89. package/templates/assistkick-product-system/packages/frontend/src/components/HighlightedText.tsx +87 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/ImageLightbox.tsx +192 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +2 -2
  92. package/templates/assistkick-product-system/packages/frontend/src/components/MentionPill.tsx +33 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/MermaidBlock.tsx +148 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/PermissionDialog.tsx +91 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/PermissionModeSelector.tsx +229 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +249 -83
  97. package/templates/assistkick-product-system/packages/frontend/src/components/QueuedMessageBubble.tsx +38 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +212 -117
  99. package/templates/assistkick-product-system/packages/frontend/src/components/SystemPromptAccordion.tsx +48 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/TaskIcon.tsx +11 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +25 -9
  102. package/templates/assistkick-product-system/packages/frontend/src/components/ToolDiffView.tsx +114 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/ToolResultCard.tsx +87 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +149 -0
  105. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +25 -8
  106. package/templates/assistkick-product-system/packages/frontend/src/components/UnifiedGitWidget.tsx +722 -0
  107. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +2 -0
  108. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +2 -1
  109. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/ProgrammableNode.tsx +178 -0
  110. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +3 -0
  111. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +103 -9
  112. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +26 -2
  113. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +42 -1
  114. package/templates/assistkick-product-system/packages/frontend/src/hooks/useDocumentTitle.ts +11 -0
  115. package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +1 -0
  116. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_chat_stream.ts +826 -0
  117. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_file_tree_cache.ts +69 -0
  118. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_mention_autocomplete.ts +284 -0
  119. package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.test.ts +183 -0
  120. package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.ts +150 -0
  121. package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.test.ts +305 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.ts +113 -0
  123. package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.test.ts +157 -0
  124. package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.ts +95 -0
  125. package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.test.ts +65 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.ts +110 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/lib/message_queue.ts +66 -0
  128. package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.test.ts +124 -0
  129. package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.ts +112 -0
  130. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +2 -0
  131. package/templates/assistkick-product-system/packages/frontend/src/routes/ChatRoute.tsx +8 -0
  132. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +2 -0
  133. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +0 -4
  134. package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +2 -0
  135. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +2 -0
  136. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -0
  137. package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +2 -0
  138. package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +2 -0
  139. package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +2 -0
  140. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +2 -0
  141. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +2 -0
  142. package/templates/assistkick-product-system/packages/frontend/src/routes/accept_invitation.tsx +2 -0
  143. package/templates/assistkick-product-system/packages/frontend/src/routes/forgot_password.tsx +2 -0
  144. package/templates/assistkick-product-system/packages/frontend/src/routes/login.tsx +2 -0
  145. package/templates/assistkick-product-system/packages/frontend/src/routes/register.tsx +2 -0
  146. package/templates/assistkick-product-system/packages/frontend/src/routes/reset_password.tsx +2 -0
  147. package/templates/assistkick-product-system/packages/frontend/src/stores/useAttachmentStore.ts +66 -0
  148. package/templates/assistkick-product-system/packages/frontend/src/stores/useChatSessionStore.ts +107 -0
  149. package/templates/assistkick-product-system/packages/frontend/src/stores/useMessageQueueStore.ts +110 -0
  150. package/templates/assistkick-product-system/packages/frontend/src/stores/usePreviewStore.ts +78 -0
  151. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +7 -0
  152. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +6 -1
  153. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +30 -357
  154. package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.test.ts +115 -0
  155. package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.ts +91 -0
  156. package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.test.ts +30 -0
  157. package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.ts +3 -0
  158. package/templates/assistkick-product-system/packages/shared/db/migrate.ts +82 -0
  159. package/templates/assistkick-product-system/packages/shared/db/migrations/0000_outgoing_ultron.sql +277 -0
  160. package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
  161. package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
  162. package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
  163. package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
  164. package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
  165. package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
  166. package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
  167. package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
  168. package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
  169. package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
  170. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0000_snapshot.json +972 -22
  171. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
  172. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
  173. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
  174. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
  175. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
  176. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
  177. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
  178. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
  179. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
  180. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
  181. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +2 -100
  182. package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
  183. package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
  184. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
  185. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
  186. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
  187. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
  188. package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
  189. package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
  190. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
  191. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
  192. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
  193. package/templates/assistkick-product-system/packages/shared/package.json +2 -1
  194. package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
  195. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
  196. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
  197. package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
  198. package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
  199. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
  200. package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
  201. package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
  202. package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
  203. package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
  204. package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
  205. package/templates/skills/assistkick-debugger/SKILL.md +23 -23
  206. package/templates/skills/assistkick-developer/SKILL.md +59 -63
  207. package/templates/skills/assistkick-interview/SKILL.md +26 -26
  208. package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
  209. package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
@@ -1,7 +1,24 @@
1
- import { describe, it, mock, beforeEach } from 'node:test';
1
+ import { describe, it, mock, beforeEach, after } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
+ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
3
6
  import {WorkflowEngine} from "./workflow_engine.js";
4
7
 
8
+ // ── Test Skills Directory ────────────────────────────────────────────
9
+ const TEST_SKILLS_DIR = mkdtempSync(join(tmpdir(), 'wf-engine-test-skills-'));
10
+
11
+ // Create test skill files
12
+ for (const skillId of ['assistkick-developer', 'assistkick-code-reviewer', 'assistkick-debugger']) {
13
+ const dir = join(TEST_SKILLS_DIR, skillId);
14
+ mkdirSync(dir, { recursive: true });
15
+ writeFileSync(join(dir, 'SKILL.md'), `# ${skillId} skill instructions`);
16
+ }
17
+
18
+ after(() => {
19
+ rmSync(TEST_SKILLS_DIR, { recursive: true, force: true });
20
+ });
21
+
5
22
  // ── Default workflow graph (matches seeded data) ──────────────────────
6
23
 
7
24
  const DEFAULT_GRAPH = {
@@ -103,14 +120,20 @@ const createMockDb = (graphData = DEFAULT_GRAPH) => {
103
120
  'dev-agent-id': {
104
121
  id: 'dev-agent-id',
105
122
  name: 'Developer',
106
- promptTemplate: 'Implement feature {{featureId}} in {{worktreePath}}',
123
+ model: 'claude-opus-4-6',
124
+ skills: '["assistkick-developer"]',
125
+ grounding: 'Implement feature {{featureId}} in {{worktreePath}}',
126
+ defaultGrounding: 'Implement feature {{featureId}} in {{worktreePath}}',
107
127
  projectId: null,
108
128
  isDefault: 1,
109
129
  },
110
130
  'rev-agent-id': {
111
131
  id: 'rev-agent-id',
112
132
  name: 'Reviewer',
113
- promptTemplate: 'Review feature {{featureId}}',
133
+ model: 'claude-opus-4-6',
134
+ skills: '["assistkick-code-reviewer"]',
135
+ grounding: 'Review feature {{featureId}}',
136
+ defaultGrounding: 'Review feature {{featureId}}',
114
137
  projectId: null,
115
138
  isDefault: 1,
116
139
  },
@@ -243,7 +266,7 @@ const createMockClaudeService = () => ({
243
266
  });
244
267
 
245
268
  const createMockGitWorkflow = () => ({
246
- createWorktree: mock.fn(async (featureId: string) => `/tmp/worktrees/${featureId}`),
269
+ createWorktree: mock.fn(async (featureId: string, _projectId: string) => `/tmp/worktrees/${featureId}`),
247
270
  removeWorktree: mock.fn(async () => {}),
248
271
  deleteBranch: mock.fn(async () => {}),
249
272
  commitChanges: mock.fn(async () => {}),
@@ -253,6 +276,8 @@ const createMockGitWorkflow = () => ({
253
276
  stash: mock.fn(async () => false),
254
277
  unstash: mock.fn(async () => {}),
255
278
  getDirtyFiles: mock.fn(async () => []),
279
+ pullDefaultBranch: mock.fn(async () => {}),
280
+ pushToRemote: mock.fn(async () => {}),
256
281
  });
257
282
 
258
283
  const createMockLog = () => mock.fn((_tag: string, _msg: string) => {});
@@ -277,6 +302,7 @@ describe('WorkflowEngine', () => {
277
302
  kanban: createMockKanban(),
278
303
  claudeService: createMockClaudeService(),
279
304
  gitWorkflow: createMockGitWorkflow(),
305
+ skillsDir: TEST_SKILLS_DIR,
280
306
  log: createMockLog(),
281
307
  });
282
308
 
@@ -294,6 +320,7 @@ describe('WorkflowEngine', () => {
294
320
  kanban: createMockKanban(),
295
321
  claudeService: createMockClaudeService(),
296
322
  gitWorkflow: createMockGitWorkflow(),
323
+ skillsDir: TEST_SKILLS_DIR,
297
324
  log: createMockLog(),
298
325
  });
299
326
 
@@ -311,6 +338,7 @@ describe('WorkflowEngine', () => {
311
338
  kanban: createMockKanban(),
312
339
  claudeService: createMockClaudeService(),
313
340
  gitWorkflow: createMockGitWorkflow(),
341
+ skillsDir: TEST_SKILLS_DIR,
314
342
  log: createMockLog(),
315
343
  });
316
344
 
@@ -344,6 +372,7 @@ describe('WorkflowEngine', () => {
344
372
  kanban: createMockKanban(),
345
373
  claudeService: createMockClaudeService(),
346
374
  gitWorkflow: createMockGitWorkflow(),
375
+ skillsDir: TEST_SKILLS_DIR,
347
376
  log: createMockLog(),
348
377
  });
349
378
 
@@ -357,6 +386,7 @@ describe('WorkflowEngine', () => {
357
386
  kanban: createMockKanban(),
358
387
  claudeService: createMockClaudeService(),
359
388
  gitWorkflow: createMockGitWorkflow(),
389
+ skillsDir: TEST_SKILLS_DIR,
360
390
  log: createMockLog(),
361
391
  });
362
392
 
@@ -370,6 +400,7 @@ describe('WorkflowEngine', () => {
370
400
  kanban: createMockKanban(),
371
401
  claudeService: createMockClaudeService(),
372
402
  gitWorkflow: createMockGitWorkflow(),
403
+ skillsDir: TEST_SKILLS_DIR,
373
404
  log: createMockLog(),
374
405
  });
375
406
 
@@ -383,6 +414,7 @@ describe('WorkflowEngine', () => {
383
414
  kanban: createMockKanban(),
384
415
  claudeService: createMockClaudeService(),
385
416
  gitWorkflow: createMockGitWorkflow(),
417
+ skillsDir: TEST_SKILLS_DIR,
386
418
  log: createMockLog(),
387
419
  });
388
420
 
@@ -396,6 +428,7 @@ describe('WorkflowEngine', () => {
396
428
  kanban: createMockKanban(),
397
429
  claudeService: createMockClaudeService(),
398
430
  gitWorkflow: createMockGitWorkflow(),
431
+ skillsDir: TEST_SKILLS_DIR,
399
432
  log: createMockLog(),
400
433
  });
401
434
 
@@ -409,6 +442,7 @@ describe('WorkflowEngine', () => {
409
442
  kanban: createMockKanban(),
410
443
  claudeService: createMockClaudeService(),
411
444
  gitWorkflow: createMockGitWorkflow(),
445
+ skillsDir: TEST_SKILLS_DIR,
412
446
  log: createMockLog(),
413
447
  });
414
448
 
@@ -422,6 +456,7 @@ describe('WorkflowEngine', () => {
422
456
  kanban: createMockKanban(),
423
457
  claudeService: createMockClaudeService(),
424
458
  gitWorkflow: createMockGitWorkflow(),
459
+ skillsDir: TEST_SKILLS_DIR,
425
460
  log: createMockLog(),
426
461
  });
427
462
 
@@ -439,6 +474,7 @@ describe('WorkflowEngine', () => {
439
474
  kanban,
440
475
  claudeService: createMockClaudeService(),
441
476
  gitWorkflow: createMockGitWorkflow(),
477
+ skillsDir: TEST_SKILLS_DIR,
442
478
  log: createMockLog(),
443
479
  });
444
480
 
@@ -458,6 +494,7 @@ describe('WorkflowEngine', () => {
458
494
  kanban,
459
495
  claudeService: createMockClaudeService(),
460
496
  gitWorkflow: createMockGitWorkflow(),
497
+ skillsDir: TEST_SKILLS_DIR,
461
498
  log: createMockLog(),
462
499
  });
463
500
 
@@ -476,6 +513,7 @@ describe('WorkflowEngine', () => {
476
513
  kanban,
477
514
  claudeService: createMockClaudeService(),
478
515
  gitWorkflow: createMockGitWorkflow(),
516
+ skillsDir: TEST_SKILLS_DIR,
479
517
  log: createMockLog(),
480
518
  });
481
519
 
@@ -496,6 +534,7 @@ describe('WorkflowEngine', () => {
496
534
  kanban,
497
535
  claudeService: createMockClaudeService(),
498
536
  gitWorkflow: createMockGitWorkflow(),
537
+ skillsDir: TEST_SKILLS_DIR,
499
538
  log: createMockLog(),
500
539
  });
501
540
 
@@ -512,6 +551,7 @@ describe('WorkflowEngine', () => {
512
551
  kanban,
513
552
  claudeService: createMockClaudeService(),
514
553
  gitWorkflow: createMockGitWorkflow(),
554
+ skillsDir: TEST_SKILLS_DIR,
515
555
  log: createMockLog(),
516
556
  });
517
557
 
@@ -529,6 +569,7 @@ describe('WorkflowEngine', () => {
529
569
  kanban,
530
570
  claudeService: createMockClaudeService(),
531
571
  gitWorkflow: createMockGitWorkflow(),
572
+ skillsDir: TEST_SKILLS_DIR,
532
573
  log: createMockLog(),
533
574
  });
534
575
 
@@ -546,6 +587,7 @@ describe('WorkflowEngine', () => {
546
587
  kanban: createMockKanban(),
547
588
  claudeService: createMockClaudeService(),
548
589
  gitWorkflow: createMockGitWorkflow(),
590
+ skillsDir: TEST_SKILLS_DIR,
549
591
  log: createMockLog(),
550
592
  });
551
593
 
@@ -563,6 +605,7 @@ describe('WorkflowEngine', () => {
563
605
  kanban: createMockKanban(),
564
606
  claudeService: createMockClaudeService(),
565
607
  gitWorkflow: createMockGitWorkflow(),
608
+ skillsDir: TEST_SKILLS_DIR,
566
609
  log: createMockLog(),
567
610
  });
568
611
 
@@ -580,6 +623,7 @@ describe('WorkflowEngine', () => {
580
623
  kanban: createMockKanban(),
581
624
  claudeService: createMockClaudeService(),
582
625
  gitWorkflow: createMockGitWorkflow(),
626
+ skillsDir: TEST_SKILLS_DIR,
583
627
  log: createMockLog(),
584
628
  });
585
629
 
@@ -601,6 +645,7 @@ describe('WorkflowEngine', () => {
601
645
  kanban,
602
646
  claudeService: createMockClaudeService(),
603
647
  gitWorkflow: createMockGitWorkflow(),
648
+ skillsDir: TEST_SKILLS_DIR,
604
649
  log: createMockLog(),
605
650
  });
606
651
 
@@ -618,6 +663,7 @@ describe('WorkflowEngine', () => {
618
663
  kanban,
619
664
  claudeService: createMockClaudeService(),
620
665
  gitWorkflow: createMockGitWorkflow(),
666
+ skillsDir: TEST_SKILLS_DIR,
621
667
  log: createMockLog(),
622
668
  });
623
669
 
@@ -635,6 +681,7 @@ describe('WorkflowEngine', () => {
635
681
  kanban: createMockKanban(),
636
682
  claudeService: createMockClaudeService(),
637
683
  gitWorkflow: createMockGitWorkflow(),
684
+ skillsDir: TEST_SKILLS_DIR,
638
685
  log: createMockLog(),
639
686
  });
640
687
 
@@ -659,7 +706,60 @@ describe('WorkflowEngine', () => {
659
706
 
660
707
  const engine = new WorkflowEngine({
661
708
  db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
662
- gitWorkflow, log: createMockLog(),
709
+ gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
710
+ });
711
+
712
+ const node = { id: 'end_success', type: 'end', data: { outcome: 'success' } };
713
+ const context = defaultContext({ cycle: 1 });
714
+
715
+ await (engine as any).handleEnd(node, context, execId);
716
+
717
+ assert.equal(gitWorkflow.mergeBranch.mock.calls.length, 1);
718
+ assert.equal(gitWorkflow.removeWorktree.mock.calls.length, 1);
719
+ assert.equal(gitWorkflow.deleteBranch.mock.calls.length, 1);
720
+ assert.equal(db._stores.executionsStore[execId].status, 'completed');
721
+ });
722
+
723
+ it('pushes to remote after successful merge', async () => {
724
+ const db = createMockDb();
725
+ const gitWorkflow = createMockGitWorkflow();
726
+ const execId = 'test-exec-push';
727
+
728
+ db._stores.executionsStore[execId] = {
729
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
730
+ status: 'running', currentNodeId: 'end_success',
731
+ context: '{}', startedAt: '2026-01-01', updatedAt: '2026-01-01',
732
+ };
733
+
734
+ const engine = new WorkflowEngine({
735
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
736
+ gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
737
+ });
738
+
739
+ const node = { id: 'end_success', type: 'end', data: { outcome: 'success' } };
740
+ const context = defaultContext({ cycle: 1, projectId: 'proj_test' });
741
+
742
+ await (engine as any).handleEnd(node, context, execId);
743
+
744
+ assert.equal(gitWorkflow.pushToRemote.mock.calls.length, 1);
745
+ assert.equal(gitWorkflow.pushToRemote.mock.calls[0].arguments[0], 'proj_test');
746
+ });
747
+
748
+ it('does not fail workflow when push to remote fails', async () => {
749
+ const db = createMockDb();
750
+ const gitWorkflow = createMockGitWorkflow();
751
+ gitWorkflow.pushToRemote = mock.fn(async () => { throw new Error('remote unreachable'); });
752
+ const execId = 'test-exec-push-fail';
753
+
754
+ db._stores.executionsStore[execId] = {
755
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
756
+ status: 'running', currentNodeId: 'end_success',
757
+ context: '{}', startedAt: '2026-01-01', updatedAt: '2026-01-01',
758
+ };
759
+
760
+ const engine = new WorkflowEngine({
761
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
762
+ gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
663
763
  });
664
764
 
665
765
  const node = { id: 'end_success', type: 'end', data: { outcome: 'success' } };
@@ -667,12 +767,37 @@ describe('WorkflowEngine', () => {
667
767
 
668
768
  await (engine as any).handleEnd(node, context, execId);
669
769
 
770
+ // Merge still succeeded and cleanup happened
670
771
  assert.equal(gitWorkflow.mergeBranch.mock.calls.length, 1);
671
772
  assert.equal(gitWorkflow.removeWorktree.mock.calls.length, 1);
672
773
  assert.equal(gitWorkflow.deleteBranch.mock.calls.length, 1);
673
774
  assert.equal(db._stores.executionsStore[execId].status, 'completed');
674
775
  });
675
776
 
777
+ it('does not push to remote on blocked outcome', async () => {
778
+ const db = createMockDb();
779
+ const gitWorkflow = createMockGitWorkflow();
780
+ const execId = 'test-exec-no-push';
781
+
782
+ db._stores.executionsStore[execId] = {
783
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
784
+ status: 'running', currentNodeId: 'end_blocked',
785
+ context: '{}', startedAt: '2026-01-01', updatedAt: '2026-01-01',
786
+ };
787
+
788
+ const engine = new WorkflowEngine({
789
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
790
+ gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
791
+ });
792
+
793
+ const node = { id: 'end_blocked', type: 'end', data: { outcome: 'blocked' } };
794
+ const context = defaultContext({ cycle: 3 });
795
+
796
+ await (engine as any).handleEnd(node, context, execId);
797
+
798
+ assert.equal(gitWorkflow.pushToRemote.mock.calls.length, 0);
799
+ });
800
+
676
801
  it('cleans up worktree without merge and sets completed on blocked outcome', async () => {
677
802
  const db = createMockDb();
678
803
  const gitWorkflow = createMockGitWorkflow();
@@ -686,7 +811,7 @@ describe('WorkflowEngine', () => {
686
811
 
687
812
  const engine = new WorkflowEngine({
688
813
  db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
689
- gitWorkflow, log: createMockLog(),
814
+ gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
690
815
  });
691
816
 
692
817
  const node = { id: 'end_blocked', type: 'end', data: { outcome: 'blocked' } };
@@ -713,7 +838,7 @@ describe('WorkflowEngine', () => {
713
838
 
714
839
  const engine = new WorkflowEngine({
715
840
  db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
716
- gitWorkflow, log: createMockLog(),
841
+ gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
717
842
  });
718
843
 
719
844
  const node = { id: 'end_fail', type: 'end', data: { outcome: 'failed' } };
@@ -735,7 +860,7 @@ describe('WorkflowEngine', () => {
735
860
 
736
861
  const engine = new WorkflowEngine({
737
862
  db: db as any, kanban: createMockKanban(), claudeService,
738
- gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
863
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
739
864
  });
740
865
 
741
866
  const node = { id: 'run1', type: 'runAgent', data: { agentId: 'dev-agent-id' } };
@@ -749,10 +874,14 @@ describe('WorkflowEngine', () => {
749
874
  assert.equal(result.outputData.agentId, 'dev-agent-id');
750
875
  assert.equal(result.outputData.agentName, 'Developer');
751
876
 
752
- // Verify spawnClaude called with resolved prompt and worktree path
877
+ // Verify spawnClaude called with composed prompt (skill content + resolved grounding) and worktree path
753
878
  const call = claudeService.spawnClaude.mock.calls[0];
754
- assert.equal(call.arguments[0], 'Implement feature feat_test in /tmp/wt');
879
+ const prompt = call.arguments[0] as string;
880
+ assert.ok(prompt.includes('# assistkick-developer skill instructions'), 'prompt should include skill file content');
881
+ assert.ok(prompt.includes('Implement feature feat_test in /tmp/wt'), 'prompt should include resolved grounding');
755
882
  assert.equal(call.arguments[1], '/tmp/wt');
883
+ // Verify model is passed through
884
+ assert.equal(call.arguments[3].model, 'claude-opus-4-6');
756
885
  });
757
886
 
758
887
  it('passes onToolUse and onResult callbacks to spawnClaude', async () => {
@@ -781,7 +910,7 @@ describe('WorkflowEngine', () => {
781
910
 
782
911
  const engine = new WorkflowEngine({
783
912
  db: db as any, kanban: createMockKanban(), claudeService,
784
- gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
913
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
785
914
  });
786
915
 
787
916
  const node = { id: 'run1', type: 'runAgent', data: { agentId: 'dev-agent-id' } };
@@ -806,7 +935,7 @@ describe('WorkflowEngine', () => {
806
935
 
807
936
  const engine = new WorkflowEngine({
808
937
  db: db as any, kanban: createMockKanban(), claudeService,
809
- gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
938
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
810
939
  });
811
940
 
812
941
  const node = { id: 'run1', type: 'runAgent', data: { agentId: 'dev-agent-id' } };
@@ -831,7 +960,7 @@ describe('WorkflowEngine', () => {
831
960
 
832
961
  const engine = new WorkflowEngine({
833
962
  db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
834
- gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
963
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
835
964
  });
836
965
 
837
966
  const node = { id: 'run1', type: 'runAgent', data: { agentId: 'nonexistent-id' } };
@@ -848,7 +977,10 @@ describe('WorkflowEngine', () => {
848
977
  db._stores.agentsStore['custom-agent'] = {
849
978
  id: 'custom-agent',
850
979
  name: 'Custom',
851
- promptTemplate: 'Feature: {{featureId}}, Branch: {{branchName}}, Cycle: {{cycle}}',
980
+ model: 'claude-opus-4-6',
981
+ skills: '[]',
982
+ grounding: 'Feature: {{featureId}}, Branch: {{branchName}}, Cycle: {{cycle}}',
983
+ defaultGrounding: null,
852
984
  projectId: null,
853
985
  isDefault: 0,
854
986
  };
@@ -856,7 +988,7 @@ describe('WorkflowEngine', () => {
856
988
 
857
989
  const engine = new WorkflowEngine({
858
990
  db: db as any, kanban: createMockKanban(), claudeService,
859
- gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
991
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
860
992
  });
861
993
 
862
994
  const node = { id: 'run1', type: 'runAgent', data: { agentId: 'custom-agent' } };
@@ -867,6 +999,123 @@ describe('WorkflowEngine', () => {
867
999
  const call = claudeService.spawnClaude.mock.calls[0];
868
1000
  assert.equal(call.arguments[0], 'Feature: feat_test, Branch: feature/feat_test, Cycle: 2');
869
1001
  });
1002
+
1003
+ it('composes prompt from skill files and resolved grounding', async () => {
1004
+ const db = createMockDb();
1005
+ const claudeService = createMockClaudeService();
1006
+ claudeService.spawnClaude = mock.fn(async () => 'output');
1007
+
1008
+ const engine = new WorkflowEngine({
1009
+ db: db as any, kanban: createMockKanban(), claudeService,
1010
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
1011
+ });
1012
+
1013
+ const node = { id: 'run1', type: 'runAgent', data: { agentId: 'dev-agent-id' } };
1014
+ const context = defaultContext({ cycle: 1 });
1015
+
1016
+ await (engine as any).handleRunAgent(node, context, 'exec-id');
1017
+
1018
+ const call = claudeService.spawnClaude.mock.calls[0];
1019
+ const prompt = call.arguments[0] as string;
1020
+ // Skill content comes first, grounding comes after
1021
+ const skillIdx = prompt.indexOf('# assistkick-developer skill instructions');
1022
+ const groundingIdx = prompt.indexOf('Implement feature feat_test');
1023
+ assert.ok(skillIdx >= 0, 'prompt should contain skill content');
1024
+ assert.ok(groundingIdx > skillIdx, 'grounding should come after skill content');
1025
+ });
1026
+
1027
+ it('does not resolve placeholders in skill content', async () => {
1028
+ const db = createMockDb();
1029
+ // Create a skill file with a placeholder-like pattern
1030
+ const skillDir = join(TEST_SKILLS_DIR, 'test-no-resolve');
1031
+ mkdirSync(skillDir, { recursive: true });
1032
+ writeFileSync(join(skillDir, 'SKILL.md'), 'Skill with {{featureId}} in it');
1033
+
1034
+ db._stores.agentsStore['skill-no-resolve'] = {
1035
+ id: 'skill-no-resolve',
1036
+ name: 'TestNoResolve',
1037
+ model: 'claude-opus-4-6',
1038
+ skills: '["test-no-resolve"]',
1039
+ grounding: 'Grounding for {{featureId}}',
1040
+ defaultGrounding: null,
1041
+ projectId: null,
1042
+ isDefault: 0,
1043
+ };
1044
+ const claudeService = createMockClaudeService();
1045
+ claudeService.spawnClaude = mock.fn(async () => 'output');
1046
+
1047
+ const engine = new WorkflowEngine({
1048
+ db: db as any, kanban: createMockKanban(), claudeService,
1049
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
1050
+ });
1051
+
1052
+ const node = { id: 'run1', type: 'runAgent', data: { agentId: 'skill-no-resolve' } };
1053
+ const context = defaultContext({ cycle: 1 });
1054
+
1055
+ await (engine as any).handleRunAgent(node, context, 'exec-id');
1056
+
1057
+ const prompt = claudeService.spawnClaude.mock.calls[0].arguments[0] as string;
1058
+ // Skill content should NOT have {{featureId}} resolved
1059
+ assert.ok(prompt.includes('Skill with {{featureId}} in it'), 'skill content should not be interpolated');
1060
+ // Grounding should have {{featureId}} resolved
1061
+ assert.ok(prompt.includes('Grounding for feat_test'), 'grounding should be interpolated');
1062
+ });
1063
+
1064
+ it('passes agent model to spawnClaude', async () => {
1065
+ const db = createMockDb();
1066
+ db._stores.agentsStore['custom-model'] = {
1067
+ id: 'custom-model',
1068
+ name: 'CustomModel',
1069
+ model: 'claude-sonnet-4-6',
1070
+ skills: '[]',
1071
+ grounding: 'test grounding',
1072
+ defaultGrounding: null,
1073
+ projectId: null,
1074
+ isDefault: 0,
1075
+ };
1076
+ const claudeService = createMockClaudeService();
1077
+ claudeService.spawnClaude = mock.fn(async () => 'output');
1078
+
1079
+ const engine = new WorkflowEngine({
1080
+ db: db as any, kanban: createMockKanban(), claudeService,
1081
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
1082
+ });
1083
+
1084
+ const node = { id: 'run1', type: 'runAgent', data: { agentId: 'custom-model' } };
1085
+ const context = defaultContext({ cycle: 1 });
1086
+
1087
+ await (engine as any).handleRunAgent(node, context, 'exec-id');
1088
+
1089
+ const opts = claudeService.spawnClaude.mock.calls[0].arguments[3];
1090
+ assert.equal(opts.model, 'claude-sonnet-4-6');
1091
+ });
1092
+
1093
+ it('throws error when skill file is not found', async () => {
1094
+ const db = createMockDb();
1095
+ db._stores.agentsStore['missing-skill'] = {
1096
+ id: 'missing-skill',
1097
+ name: 'MissingSkill',
1098
+ model: 'claude-opus-4-6',
1099
+ skills: '["nonexistent-skill"]',
1100
+ grounding: 'test',
1101
+ defaultGrounding: null,
1102
+ projectId: null,
1103
+ isDefault: 0,
1104
+ };
1105
+
1106
+ const engine = new WorkflowEngine({
1107
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
1108
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
1109
+ });
1110
+
1111
+ const node = { id: 'run1', type: 'runAgent', data: { agentId: 'missing-skill' } };
1112
+ const context = defaultContext({ cycle: 1 });
1113
+
1114
+ await assert.rejects(
1115
+ () => (engine as any).handleRunAgent(node, context, 'exec-id'),
1116
+ /Failed to read skill file for "nonexistent-skill"/,
1117
+ );
1118
+ });
870
1119
  });
871
1120
 
872
1121
  describe('getExecutionMetrics', () => {
@@ -913,7 +1162,7 @@ describe('WorkflowEngine', () => {
913
1162
 
914
1163
  const engine = new WorkflowEngine({
915
1164
  db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
916
- gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
1165
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
917
1166
  });
918
1167
 
919
1168
  const metrics = await engine.getExecutionMetrics(execId);
@@ -931,7 +1180,7 @@ describe('WorkflowEngine', () => {
931
1180
  const db = createMockDb();
932
1181
  const engine = new WorkflowEngine({
933
1182
  db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
934
- gitWorkflow: createMockGitWorkflow(), log: createMockLog(),
1183
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
935
1184
  });
936
1185
 
937
1186
  const metrics = await engine.getExecutionMetrics('nonexistent');
@@ -972,7 +1221,7 @@ describe('WorkflowEngine', () => {
972
1221
 
973
1222
  const engine = new WorkflowEngine({
974
1223
  db: db as any, kanban, claudeService: createMockClaudeService(),
975
- gitWorkflow, log: createMockLog(),
1224
+ gitWorkflow, skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
976
1225
  });
977
1226
 
978
1227
  const { executionId } = await engine.start('feat_test', WORKFLOW_ID, 'proj_test');
@@ -992,6 +1241,7 @@ describe('WorkflowEngine', () => {
992
1241
  kanban: createMockKanban(),
993
1242
  claudeService: createMockClaudeService(),
994
1243
  gitWorkflow: createMockGitWorkflow(),
1244
+ skillsDir: TEST_SKILLS_DIR,
995
1245
  log: createMockLog(),
996
1246
  });
997
1247
 
@@ -1018,6 +1268,7 @@ describe('WorkflowEngine', () => {
1018
1268
  kanban: createMockKanban(),
1019
1269
  claudeService: createMockClaudeService(),
1020
1270
  gitWorkflow: createMockGitWorkflow(),
1271
+ skillsDir: TEST_SKILLS_DIR,
1021
1272
  log: createMockLog(),
1022
1273
  });
1023
1274
 
@@ -1040,6 +1291,7 @@ describe('WorkflowEngine', () => {
1040
1291
  kanban: createMockKanban(),
1041
1292
  claudeService: createMockClaudeService(),
1042
1293
  gitWorkflow: createMockGitWorkflow(),
1294
+ skillsDir: TEST_SKILLS_DIR,
1043
1295
  log: createMockLog(),
1044
1296
  });
1045
1297
 
@@ -1070,6 +1322,7 @@ describe('WorkflowEngine', () => {
1070
1322
  db: db as any, kanban: createMockKanban(),
1071
1323
  claudeService: createMockClaudeService(),
1072
1324
  gitWorkflow: createMockGitWorkflow(),
1325
+ skillsDir: TEST_SKILLS_DIR,
1073
1326
  log: createMockLog(),
1074
1327
  });
1075
1328
 
@@ -1105,6 +1358,7 @@ describe('WorkflowEngine', () => {
1105
1358
  db: db as any, kanban: createMockKanban(),
1106
1359
  claudeService: createMockClaudeService(),
1107
1360
  gitWorkflow: createMockGitWorkflow(),
1361
+ skillsDir: TEST_SKILLS_DIR,
1108
1362
  log: createMockLog(),
1109
1363
  });
1110
1364
 
@@ -1137,7 +1391,7 @@ describe('WorkflowEngine', () => {
1137
1391
  const engine = new WorkflowEngine({
1138
1392
  db: db as any, kanban: createMockKanban(),
1139
1393
  claudeService: createMockClaudeService(), gitWorkflow,
1140
- log: createMockLog(),
1394
+ skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
1141
1395
  });
1142
1396
 
1143
1397
  const { executionId } = await engine.start('feat_test', WORKFLOW_ID, 'proj_test');
@@ -1147,11 +1401,16 @@ describe('WorkflowEngine', () => {
1147
1401
 
1148
1402
  // Execution row was created
1149
1403
  assert.ok(db._stores.executionsStore[executionId]);
1404
+ // Pull was called before worktree creation
1405
+ assert.equal(gitWorkflow.pullDefaultBranch.mock.calls.length, 1);
1406
+ assert.equal(gitWorkflow.pullDefaultBranch.mock.calls[0].arguments[0], 'proj_test');
1150
1407
  // Worktree was created
1151
1408
  assert.equal(gitWorkflow.createWorktree.mock.calls.length, 1);
1152
1409
  assert.equal(gitWorkflow.createWorktree.mock.calls[0].arguments[0], 'feat_test');
1153
1410
  // Merge was called (success outcome)
1154
1411
  assert.equal(gitWorkflow.mergeBranch.mock.calls.length, 1);
1412
+ // Push was called after merge
1413
+ assert.equal(gitWorkflow.pushToRemote.mock.calls.length, 1);
1155
1414
  // Execution completed
1156
1415
  assert.equal(db._stores.executionsStore[executionId].status, 'completed');
1157
1416
  // Node executions were created for both nodes
@@ -1177,6 +1436,7 @@ describe('WorkflowEngine', () => {
1177
1436
  db: db as any, kanban,
1178
1437
  claudeService: createMockClaudeService(),
1179
1438
  gitWorkflow: createMockGitWorkflow(),
1439
+ skillsDir: TEST_SKILLS_DIR,
1180
1440
  log: createMockLog(),
1181
1441
  });
1182
1442
 
@@ -1210,6 +1470,7 @@ describe('WorkflowEngine', () => {
1210
1470
  db: db as any, kanban: createMockKanban(),
1211
1471
  claudeService: createMockClaudeService(),
1212
1472
  gitWorkflow: createMockGitWorkflow(),
1473
+ skillsDir: TEST_SKILLS_DIR,
1213
1474
  log: createMockLog(),
1214
1475
  });
1215
1476
 
@@ -1239,6 +1500,7 @@ describe('WorkflowEngine', () => {
1239
1500
  db: db as any, kanban: createMockKanban(),
1240
1501
  claudeService: createMockClaudeService(),
1241
1502
  gitWorkflow: createMockGitWorkflow(),
1503
+ skillsDir: TEST_SKILLS_DIR,
1242
1504
  log: createMockLog(),
1243
1505
  });
1244
1506
 
@@ -1270,6 +1532,7 @@ describe('WorkflowEngine', () => {
1270
1532
  db: db as any, kanban,
1271
1533
  claudeService: createMockClaudeService(),
1272
1534
  gitWorkflow: createMockGitWorkflow(),
1535
+ skillsDir: TEST_SKILLS_DIR,
1273
1536
  log: createMockLog(),
1274
1537
  });
1275
1538
 
@@ -1301,6 +1564,7 @@ describe('WorkflowEngine', () => {
1301
1564
  db: db as any, kanban: createMockKanban(),
1302
1565
  claudeService: createMockClaudeService(),
1303
1566
  gitWorkflow: createMockGitWorkflow(),
1567
+ skillsDir: TEST_SKILLS_DIR,
1304
1568
  log: createMockLog(),
1305
1569
  });
1306
1570
 
@@ -1351,7 +1615,7 @@ describe('WorkflowEngine', () => {
1351
1615
  const engine = new WorkflowEngine({
1352
1616
  db: db as any, kanban: createMockKanban(),
1353
1617
  claudeService: createMockClaudeService(), gitWorkflow,
1354
- log: createMockLog(),
1618
+ skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
1355
1619
  });
1356
1620
 
1357
1621
  await engine.resume(execId);
@@ -1381,6 +1645,7 @@ describe('WorkflowEngine', () => {
1381
1645
  db: db as any, kanban: createMockKanban(),
1382
1646
  claudeService: createMockClaudeService(),
1383
1647
  gitWorkflow: createMockGitWorkflow(),
1648
+ skillsDir: TEST_SKILLS_DIR,
1384
1649
  log: createMockLog(),
1385
1650
  });
1386
1651
 
@@ -1406,6 +1671,7 @@ describe('WorkflowEngine', () => {
1406
1671
  db: db as any, kanban: createMockKanban(),
1407
1672
  claudeService: createMockClaudeService(),
1408
1673
  gitWorkflow: createMockGitWorkflow(),
1674
+ skillsDir: TEST_SKILLS_DIR,
1409
1675
  log: createMockLog(),
1410
1676
  });
1411
1677
 
@@ -1423,6 +1689,7 @@ describe('WorkflowEngine', () => {
1423
1689
  kanban: createMockKanban(),
1424
1690
  claudeService: createMockClaudeService(),
1425
1691
  gitWorkflow: createMockGitWorkflow(),
1692
+ skillsDir: TEST_SKILLS_DIR,
1426
1693
  log: createMockLog(),
1427
1694
  });
1428
1695
 
@@ -1442,6 +1709,7 @@ describe('WorkflowEngine', () => {
1442
1709
  kanban: createMockKanban(),
1443
1710
  claudeService: createMockClaudeService(),
1444
1711
  gitWorkflow: createMockGitWorkflow(),
1712
+ skillsDir: TEST_SKILLS_DIR,
1445
1713
  log: createMockLog(),
1446
1714
  });
1447
1715
  });
@@ -1506,6 +1774,7 @@ describe('WorkflowEngine', () => {
1506
1774
  kanban: createMockKanban(),
1507
1775
  claudeService: createMockClaudeService(),
1508
1776
  gitWorkflow: createMockGitWorkflow(),
1777
+ skillsDir: TEST_SKILLS_DIR,
1509
1778
  log: createMockLog(),
1510
1779
  });
1511
1780
  });
@@ -1549,6 +1818,7 @@ describe('WorkflowEngine', () => {
1549
1818
  kanban: createMockKanban(),
1550
1819
  claudeService: createMockClaudeService(),
1551
1820
  gitWorkflow: createMockGitWorkflow(),
1821
+ skillsDir: TEST_SKILLS_DIR,
1552
1822
  log: createMockLog(),
1553
1823
  });
1554
1824
  });
@@ -1577,6 +1847,7 @@ describe('WorkflowEngine', () => {
1577
1847
  kanban: createMockKanban(),
1578
1848
  claudeService: createMockClaudeService(),
1579
1849
  gitWorkflow: createMockGitWorkflow(),
1850
+ skillsDir: TEST_SKILLS_DIR,
1580
1851
  log: createMockLog(),
1581
1852
  });
1582
1853
 
@@ -1612,6 +1883,7 @@ describe('WorkflowEngine', () => {
1612
1883
  outputData: JSON.stringify({ result: 'ok' }),
1613
1884
  error: null,
1614
1885
  attempt: 1,
1886
+ cycle: 1,
1615
1887
  };
1616
1888
 
1617
1889
  // Seed tool calls
@@ -1635,6 +1907,7 @@ describe('WorkflowEngine', () => {
1635
1907
  kanban: createMockKanban(),
1636
1908
  claudeService: createMockClaudeService(),
1637
1909
  gitWorkflow: createMockGitWorkflow(),
1910
+ skillsDir: TEST_SKILLS_DIR,
1638
1911
  log: createMockLog(),
1639
1912
  });
1640
1913
 
@@ -1653,14 +1926,16 @@ describe('WorkflowEngine', () => {
1653
1926
  assert.ok(graphData.nodes.length > 0);
1654
1927
  assert.ok(graphData.edges.length > 0);
1655
1928
 
1656
- // Check node executions
1657
- const nodeExecs = result.nodeExecutions as Record<string, any>;
1929
+ // Check node executions — now an array per node
1930
+ const nodeExecs = result.nodeExecutions as Record<string, any[]>;
1658
1931
  assert.ok(nodeExecs['agent_dev']);
1659
- assert.equal(nodeExecs['agent_dev'].status, 'completed');
1660
- assert.deepEqual(nodeExecs['agent_dev'].outputData, { result: 'ok' });
1932
+ assert.equal(nodeExecs['agent_dev'].length, 1);
1933
+ assert.equal(nodeExecs['agent_dev'][0].status, 'completed');
1934
+ assert.equal(nodeExecs['agent_dev'][0].cycle, 1);
1935
+ assert.deepEqual(nodeExecs['agent_dev'][0].outputData, { result: 'ok' });
1661
1936
 
1662
1937
  // Check tool calls grouped under node execution
1663
- const toolCalls = nodeExecs['agent_dev'].toolCalls as any[];
1938
+ const toolCalls = nodeExecs['agent_dev'][0].toolCalls as any[];
1664
1939
  assert.equal(toolCalls.length, 2);
1665
1940
  assert.equal(toolCalls[0].toolName, 'Read');
1666
1941
  assert.equal(toolCalls[0].target, '/src/index.ts');
@@ -1700,6 +1975,7 @@ describe('WorkflowEngine', () => {
1700
1975
  kanban: createMockKanban(),
1701
1976
  claudeService: createMockClaudeService(),
1702
1977
  gitWorkflow: createMockGitWorkflow(),
1978
+ skillsDir: TEST_SKILLS_DIR,
1703
1979
  log: createMockLog(),
1704
1980
  });
1705
1981
 
@@ -1734,6 +2010,147 @@ describe('WorkflowEngine', () => {
1734
2010
  outputData: null,
1735
2011
  error: null,
1736
2012
  attempt: 1,
2013
+ cycle: 1,
2014
+ };
2015
+
2016
+ const engine = new WorkflowEngine({
2017
+ db: db as any,
2018
+ kanban: createMockKanban(),
2019
+ claudeService: createMockClaudeService(),
2020
+ gitWorkflow: createMockGitWorkflow(),
2021
+ skillsDir: TEST_SKILLS_DIR,
2022
+ log: createMockLog(),
2023
+ });
2024
+
2025
+ const result = await engine.getMonitorData('feat_test');
2026
+ assert.ok(result);
2027
+ const nodeExecs = result.nodeExecutions as Record<string, any[]>;
2028
+ assert.deepEqual(nodeExecs['transition_1'][0].toolCalls, []);
2029
+ });
2030
+
2031
+ it('returns multiple cycle executions per node sorted by cycle', async () => {
2032
+ const db = createMockDb();
2033
+ const execId = 'exec-cycles';
2034
+
2035
+ db._stores.executionsStore[execId] = {
2036
+ id: execId,
2037
+ workflowId: WORKFLOW_ID,
2038
+ featureId: 'feat_test',
2039
+ status: 'completed',
2040
+ currentNodeId: 'end_success',
2041
+ startedAt: '2026-01-01T00:00:00.000Z',
2042
+ updatedAt: '2026-01-01T00:10:00.000Z',
2043
+ contextData: null,
2044
+ };
2045
+
2046
+ // Cycle 1 execution of agent_dev
2047
+ db._stores.nodeExecutionsStore['ne-c1'] = {
2048
+ id: 'ne-c1',
2049
+ executionId: execId,
2050
+ nodeId: 'agent_dev',
2051
+ status: 'completed',
2052
+ startedAt: '2026-01-01T00:01:00.000Z',
2053
+ completedAt: '2026-01-01T00:03:00.000Z',
2054
+ outputData: JSON.stringify({ costUsd: 0.05 }),
2055
+ error: null,
2056
+ attempt: 1,
2057
+ cycle: 1,
2058
+ };
2059
+
2060
+ // Cycle 2 execution of agent_dev
2061
+ db._stores.nodeExecutionsStore['ne-c2'] = {
2062
+ id: 'ne-c2',
2063
+ executionId: execId,
2064
+ nodeId: 'agent_dev',
2065
+ status: 'completed',
2066
+ startedAt: '2026-01-01T00:05:00.000Z',
2067
+ completedAt: '2026-01-01T00:08:00.000Z',
2068
+ outputData: JSON.stringify({ costUsd: 0.08 }),
2069
+ error: null,
2070
+ attempt: 1,
2071
+ cycle: 2,
2072
+ };
2073
+
2074
+ const engine = new WorkflowEngine({
2075
+ db: db as any,
2076
+ kanban: createMockKanban(),
2077
+ claudeService: createMockClaudeService(),
2078
+ gitWorkflow: createMockGitWorkflow(),
2079
+ skillsDir: TEST_SKILLS_DIR,
2080
+ log: createMockLog(),
2081
+ });
2082
+
2083
+ const result = await engine.getMonitorData('feat_test');
2084
+ assert.ok(result);
2085
+
2086
+ const nodeExecs = result.nodeExecutions as Record<string, any[]>;
2087
+ const devExecs = nodeExecs['agent_dev'];
2088
+ assert.equal(devExecs.length, 2);
2089
+
2090
+ // Sorted by cycle ascending
2091
+ assert.equal(devExecs[0].cycle, 1);
2092
+ assert.equal(devExecs[1].cycle, 2);
2093
+
2094
+ // Each has its own data
2095
+ assert.deepEqual(devExecs[0].outputData, { costUsd: 0.05 });
2096
+ assert.deepEqual(devExecs[1].outputData, { costUsd: 0.08 });
2097
+ });
2098
+
2099
+ it('preserves tool calls per cycle execution', async () => {
2100
+ const db = createMockDb();
2101
+ const execId = 'exec-tc-cycles';
2102
+
2103
+ db._stores.executionsStore[execId] = {
2104
+ id: execId,
2105
+ workflowId: WORKFLOW_ID,
2106
+ featureId: 'feat_test',
2107
+ status: 'completed',
2108
+ currentNodeId: 'end_success',
2109
+ startedAt: '2026-01-01T00:00:00.000Z',
2110
+ updatedAt: '2026-01-01T00:10:00.000Z',
2111
+ contextData: null,
2112
+ };
2113
+
2114
+ db._stores.nodeExecutionsStore['ne-tc1'] = {
2115
+ id: 'ne-tc1',
2116
+ executionId: execId,
2117
+ nodeId: 'agent_dev',
2118
+ status: 'completed',
2119
+ startedAt: '2026-01-01T00:01:00.000Z',
2120
+ completedAt: '2026-01-01T00:03:00.000Z',
2121
+ outputData: null,
2122
+ error: null,
2123
+ attempt: 1,
2124
+ cycle: 1,
2125
+ };
2126
+
2127
+ db._stores.nodeExecutionsStore['ne-tc2'] = {
2128
+ id: 'ne-tc2',
2129
+ executionId: execId,
2130
+ nodeId: 'agent_dev',
2131
+ status: 'completed',
2132
+ startedAt: '2026-01-01T00:05:00.000Z',
2133
+ completedAt: '2026-01-01T00:08:00.000Z',
2134
+ outputData: null,
2135
+ error: null,
2136
+ attempt: 1,
2137
+ cycle: 2,
2138
+ };
2139
+
2140
+ // Tool calls for cycle 1
2141
+ db._stores.toolCallsStore['tc-c1-1'] = {
2142
+ id: 'tc-c1-1', nodeExecutionId: 'ne-tc1',
2143
+ timestamp: '2026-01-01T00:01:30.000Z', toolName: 'Read', target: '/src/a.ts',
2144
+ };
2145
+
2146
+ // Tool calls for cycle 2
2147
+ db._stores.toolCallsStore['tc-c2-1'] = {
2148
+ id: 'tc-c2-1', nodeExecutionId: 'ne-tc2',
2149
+ timestamp: '2026-01-01T00:06:00.000Z', toolName: 'Write', target: '/src/b.ts',
2150
+ };
2151
+ db._stores.toolCallsStore['tc-c2-2'] = {
2152
+ id: 'tc-c2-2', nodeExecutionId: 'ne-tc2',
2153
+ timestamp: '2026-01-01T00:07:00.000Z', toolName: 'Bash', target: 'npm test',
1737
2154
  };
1738
2155
 
1739
2156
  const engine = new WorkflowEngine({
@@ -1741,13 +2158,20 @@ describe('WorkflowEngine', () => {
1741
2158
  kanban: createMockKanban(),
1742
2159
  claudeService: createMockClaudeService(),
1743
2160
  gitWorkflow: createMockGitWorkflow(),
2161
+ skillsDir: TEST_SKILLS_DIR,
1744
2162
  log: createMockLog(),
1745
2163
  });
1746
2164
 
1747
2165
  const result = await engine.getMonitorData('feat_test');
1748
2166
  assert.ok(result);
1749
- const nodeExecs = result.nodeExecutions as Record<string, any>;
1750
- assert.deepEqual(nodeExecs['transition_1'].toolCalls, []);
2167
+
2168
+ const devExecs = (result.nodeExecutions as Record<string, any[]>)['agent_dev'];
2169
+ assert.equal(devExecs[0].toolCalls.length, 1);
2170
+ assert.equal(devExecs[0].toolCalls[0].toolName, 'Read');
2171
+
2172
+ assert.equal(devExecs[1].toolCalls.length, 2);
2173
+ assert.equal(devExecs[1].toolCalls[0].toolName, 'Write');
2174
+ assert.equal(devExecs[1].toolCalls[1].toolName, 'Bash');
1751
2175
  });
1752
2176
  });
1753
2177
 
@@ -1771,6 +2195,7 @@ describe('WorkflowEngine', () => {
1771
2195
  claudeService: createMockClaudeService(),
1772
2196
  gitWorkflow: createMockGitWorkflow(),
1773
2197
  bundleService: mockBundleService,
2198
+ skillsDir: TEST_SKILLS_DIR,
1774
2199
  log: createMockLog(),
1775
2200
  });
1776
2201
 
@@ -1799,6 +2224,7 @@ describe('WorkflowEngine', () => {
1799
2224
  claudeService: createMockClaudeService(),
1800
2225
  gitWorkflow: createMockGitWorkflow(),
1801
2226
  bundleService: mockBundleService,
2227
+ skillsDir: TEST_SKILLS_DIR,
1802
2228
  log: createMockLog(),
1803
2229
  });
1804
2230
 
@@ -1815,6 +2241,7 @@ describe('WorkflowEngine', () => {
1815
2241
  kanban: createMockKanban(),
1816
2242
  claudeService: createMockClaudeService(),
1817
2243
  gitWorkflow: createMockGitWorkflow(),
2244
+ skillsDir: TEST_SKILLS_DIR,
1818
2245
  log: createMockLog(),
1819
2246
  });
1820
2247
 
@@ -1845,6 +2272,7 @@ describe('WorkflowEngine', () => {
1845
2272
  claudeService: createMockClaudeService(),
1846
2273
  gitWorkflow: createMockGitWorkflow(),
1847
2274
  ttsService: mockTtsService,
2275
+ skillsDir: TEST_SKILLS_DIR,
1848
2276
  log: createMockLog(),
1849
2277
  });
1850
2278
 
@@ -1875,6 +2303,7 @@ describe('WorkflowEngine', () => {
1875
2303
  kanban: createMockKanban(),
1876
2304
  claudeService: createMockClaudeService(),
1877
2305
  gitWorkflow: createMockGitWorkflow(),
2306
+ skillsDir: TEST_SKILLS_DIR,
1878
2307
  log: createMockLog(),
1879
2308
  });
1880
2309
 
@@ -1905,6 +2334,7 @@ describe('WorkflowEngine', () => {
1905
2334
  claudeService: createMockClaudeService(),
1906
2335
  gitWorkflow: createMockGitWorkflow(),
1907
2336
  videoRenderService: mockVideoRenderService,
2337
+ skillsDir: TEST_SKILLS_DIR,
1908
2338
  log: createMockLog(),
1909
2339
  });
1910
2340
 
@@ -1946,6 +2376,7 @@ describe('WorkflowEngine', () => {
1946
2376
  claudeService: createMockClaudeService(),
1947
2377
  gitWorkflow: createMockGitWorkflow(),
1948
2378
  videoRenderService: mockVideoRenderService,
2379
+ skillsDir: TEST_SKILLS_DIR,
1949
2380
  log: createMockLog(),
1950
2381
  });
1951
2382
 
@@ -1964,6 +2395,7 @@ describe('WorkflowEngine', () => {
1964
2395
  kanban: createMockKanban(),
1965
2396
  claudeService: createMockClaudeService(),
1966
2397
  gitWorkflow: createMockGitWorkflow(),
2398
+ skillsDir: TEST_SKILLS_DIR,
1967
2399
  log: createMockLog(),
1968
2400
  });
1969
2401
 
@@ -1985,6 +2417,7 @@ describe('WorkflowEngine', () => {
1985
2417
  claudeService: createMockClaudeService(),
1986
2418
  gitWorkflow: createMockGitWorkflow(),
1987
2419
  videoRenderService: mockVideoRenderService,
2420
+ skillsDir: TEST_SKILLS_DIR,
1988
2421
  log: createMockLog(),
1989
2422
  });
1990
2423
 
@@ -1996,4 +2429,347 @@ describe('WorkflowEngine', () => {
1996
2429
  );
1997
2430
  });
1998
2431
  });
2432
+
2433
+ describe('stopNode', () => {
2434
+ it('marks node execution as stopped and workflow execution as failed', async () => {
2435
+ const db = createMockDb();
2436
+ const execId = 'exec-stop-1';
2437
+ const nodeExecId = 'ne-stop-1';
2438
+
2439
+ db._stores.executionsStore[execId] = {
2440
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
2441
+ status: 'running', currentNodeId: 'agent_dev',
2442
+ context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
2443
+ };
2444
+ db._stores.nodeExecutionsStore[nodeExecId] = {
2445
+ id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
2446
+ status: 'running', startedAt: '2026-01-01', completedAt: null,
2447
+ outputData: null, error: null, attempt: 1,
2448
+ };
2449
+
2450
+ const engine = new WorkflowEngine({
2451
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
2452
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
2453
+ });
2454
+
2455
+ await engine.stopNode('feat_test', 'agent_dev');
2456
+
2457
+ assert.equal(db._stores.nodeExecutionsStore[nodeExecId].status, 'stopped');
2458
+ assert.equal(db._stores.nodeExecutionsStore[nodeExecId].error, 'Stopped by user');
2459
+ assert.ok(db._stores.nodeExecutionsStore[nodeExecId].completedAt);
2460
+ assert.equal(db._stores.executionsStore[execId].status, 'failed');
2461
+ });
2462
+
2463
+ it('throws when no running execution exists', async () => {
2464
+ const db = createMockDb();
2465
+ const execId = 'exec-stop-2';
2466
+
2467
+ db._stores.executionsStore[execId] = {
2468
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
2469
+ status: 'completed', currentNodeId: null,
2470
+ context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
2471
+ };
2472
+
2473
+ const engine = new WorkflowEngine({
2474
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
2475
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
2476
+ });
2477
+
2478
+ await assert.rejects(
2479
+ () => engine.stopNode('feat_test', 'agent_dev'),
2480
+ /No running execution found/,
2481
+ );
2482
+ });
2483
+
2484
+ it('throws when nodeId is not the current node', async () => {
2485
+ const db = createMockDb();
2486
+ const execId = 'exec-stop-3';
2487
+
2488
+ db._stores.executionsStore[execId] = {
2489
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
2490
+ status: 'running', currentNodeId: 'agent_rev',
2491
+ context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
2492
+ };
2493
+
2494
+ const engine = new WorkflowEngine({
2495
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
2496
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
2497
+ });
2498
+
2499
+ await assert.rejects(
2500
+ () => engine.stopNode('feat_test', 'agent_dev'),
2501
+ /not the current execution node/,
2502
+ );
2503
+ });
2504
+
2505
+ it('kills active child process with SIGTERM', async () => {
2506
+ const db = createMockDb();
2507
+ const execId = 'exec-stop-4';
2508
+ const nodeExecId = 'ne-stop-4';
2509
+
2510
+ db._stores.executionsStore[execId] = {
2511
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
2512
+ status: 'running', currentNodeId: 'agent_dev',
2513
+ context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
2514
+ };
2515
+ db._stores.nodeExecutionsStore[nodeExecId] = {
2516
+ id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
2517
+ status: 'running', startedAt: '2026-01-01', completedAt: null,
2518
+ outputData: null, error: null, attempt: 1,
2519
+ };
2520
+
2521
+ const engine = new WorkflowEngine({
2522
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
2523
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
2524
+ });
2525
+
2526
+ // Simulate an active child process in the map
2527
+ const mockChild = { kill: mock.fn(() => true) };
2528
+ (engine as any).activeProcesses.set(execId, mockChild);
2529
+
2530
+ await engine.stopNode('feat_test', 'agent_dev');
2531
+
2532
+ assert.equal(mockChild.kill.mock.calls.length, 1);
2533
+ assert.equal(mockChild.kill.mock.calls[0].arguments[0], 'SIGTERM');
2534
+ });
2535
+
2536
+ it('adds execution to stoppedExecutions set to prevent status overwrite', async () => {
2537
+ const db = createMockDb();
2538
+ const execId = 'exec-stop-5';
2539
+ const nodeExecId = 'ne-stop-5';
2540
+
2541
+ db._stores.executionsStore[execId] = {
2542
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
2543
+ status: 'running', currentNodeId: 'agent_dev',
2544
+ context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
2545
+ };
2546
+ db._stores.nodeExecutionsStore[nodeExecId] = {
2547
+ id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
2548
+ status: 'running', startedAt: '2026-01-01', completedAt: null,
2549
+ outputData: null, error: null, attempt: 1,
2550
+ };
2551
+
2552
+ const engine = new WorkflowEngine({
2553
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
2554
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
2555
+ });
2556
+
2557
+ await engine.stopNode('feat_test', 'agent_dev');
2558
+
2559
+ assert.ok((engine as any).stoppedExecutions.has(execId));
2560
+ });
2561
+ });
2562
+
2563
+ describe('continueNode', () => {
2564
+ it('resets node execution to running and workflow execution to running', async () => {
2565
+ const db = createMockDb();
2566
+ const execId = 'exec-cont-1';
2567
+ const nodeExecId = 'ne-cont-1';
2568
+ const sessionId = 'session-abc-123';
2569
+
2570
+ db._stores.executionsStore[execId] = {
2571
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
2572
+ status: 'failed', currentNodeId: 'agent_dev',
2573
+ context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
2574
+ };
2575
+ db._stores.nodeExecutionsStore[nodeExecId] = {
2576
+ id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
2577
+ status: 'failed', startedAt: '2026-01-01', completedAt: '2026-01-01T00:05:00Z',
2578
+ outputData: null, error: 'Process died', attempt: 1, claudeSessionId: sessionId,
2579
+ };
2580
+
2581
+ const engine = new WorkflowEngine({
2582
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
2583
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
2584
+ });
2585
+
2586
+ await engine.continueNode('feat_test', 'agent_dev');
2587
+
2588
+ // Node execution should be reset to running
2589
+ assert.equal(db._stores.nodeExecutionsStore[nodeExecId].status, 'running');
2590
+ assert.equal(db._stores.nodeExecutionsStore[nodeExecId].error, null);
2591
+ assert.equal(db._stores.nodeExecutionsStore[nodeExecId].completedAt, null);
2592
+ // Attempt should NOT be incremented (continuation, not retry)
2593
+ assert.equal(db._stores.nodeExecutionsStore[nodeExecId].attempt, 1);
2594
+ // Workflow execution should be set back to running
2595
+ assert.equal(db._stores.executionsStore[execId].status, 'running');
2596
+ });
2597
+
2598
+ it('throws when no execution exists for the feature', async () => {
2599
+ const db = createMockDb();
2600
+
2601
+ const engine = new WorkflowEngine({
2602
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
2603
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
2604
+ });
2605
+
2606
+ await assert.rejects(
2607
+ () => engine.continueNode('feat_nonexistent', 'agent_dev'),
2608
+ /No execution found/,
2609
+ );
2610
+ });
2611
+
2612
+ it('throws when nodeId is not the current node', async () => {
2613
+ const db = createMockDb();
2614
+ const execId = 'exec-cont-3';
2615
+
2616
+ db._stores.executionsStore[execId] = {
2617
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
2618
+ status: 'failed', currentNodeId: 'agent_rev',
2619
+ context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
2620
+ };
2621
+
2622
+ const engine = new WorkflowEngine({
2623
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
2624
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
2625
+ });
2626
+
2627
+ await assert.rejects(
2628
+ () => engine.continueNode('feat_test', 'agent_dev'),
2629
+ /not the current execution node/,
2630
+ );
2631
+ });
2632
+
2633
+ it('throws when node has no claudeSessionId', async () => {
2634
+ const db = createMockDb();
2635
+ const execId = 'exec-cont-4';
2636
+ const nodeExecId = 'ne-cont-4';
2637
+
2638
+ db._stores.executionsStore[execId] = {
2639
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
2640
+ status: 'failed', currentNodeId: 'agent_dev',
2641
+ context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
2642
+ };
2643
+ db._stores.nodeExecutionsStore[nodeExecId] = {
2644
+ id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
2645
+ status: 'failed', startedAt: '2026-01-01', completedAt: null,
2646
+ outputData: null, error: 'Process died', attempt: 1, claudeSessionId: null,
2647
+ };
2648
+
2649
+ const engine = new WorkflowEngine({
2650
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
2651
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
2652
+ });
2653
+
2654
+ await assert.rejects(
2655
+ () => engine.continueNode('feat_test', 'agent_dev'),
2656
+ /no Claude session ID/,
2657
+ );
2658
+ });
2659
+
2660
+ it('calls spawnClaude with resume sessionId and "Continue" prompt', async () => {
2661
+ const db = createMockDb();
2662
+ const execId = 'exec-cont-5';
2663
+ const nodeExecId = 'ne-cont-5';
2664
+ const sessionId = 'session-resume-456';
2665
+
2666
+ db._stores.executionsStore[execId] = {
2667
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
2668
+ status: 'failed', currentNodeId: 'agent_dev',
2669
+ context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
2670
+ };
2671
+ db._stores.nodeExecutionsStore[nodeExecId] = {
2672
+ id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
2673
+ status: 'failed', startedAt: '2026-01-01', completedAt: null,
2674
+ outputData: null, error: 'Process died', attempt: 1, claudeSessionId: sessionId,
2675
+ };
2676
+
2677
+ const claudeService = createMockClaudeService();
2678
+ const engine = new WorkflowEngine({
2679
+ db: db as any, kanban: createMockKanban(), claudeService,
2680
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
2681
+ });
2682
+
2683
+ await engine.continueNode('feat_test', 'agent_dev');
2684
+ // Allow the fire-and-forget async to run
2685
+ await new Promise(r => setTimeout(r, 50));
2686
+
2687
+ assert.equal(claudeService.spawnClaude.mock.calls.length, 1);
2688
+ const call = claudeService.spawnClaude.mock.calls[0];
2689
+ // First arg is the prompt "Continue"
2690
+ assert.equal(call.arguments[0], 'Continue');
2691
+ // Fourth arg is the options object with resume
2692
+ assert.equal(call.arguments[3].resume, sessionId);
2693
+ });
2694
+
2695
+ it('kills existing alive process before resuming', async () => {
2696
+ const db = createMockDb();
2697
+ const execId = 'exec-cont-6';
2698
+ const nodeExecId = 'ne-cont-6';
2699
+ const sessionId = 'session-kill-789';
2700
+
2701
+ db._stores.executionsStore[execId] = {
2702
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
2703
+ status: 'running', currentNodeId: 'agent_dev',
2704
+ context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
2705
+ };
2706
+ db._stores.nodeExecutionsStore[nodeExecId] = {
2707
+ id: nodeExecId, executionId: execId, nodeId: 'agent_dev',
2708
+ status: 'running', startedAt: '2026-01-01', completedAt: null,
2709
+ outputData: null, error: null, attempt: 1, claudeSessionId: sessionId,
2710
+ };
2711
+
2712
+ const engine = new WorkflowEngine({
2713
+ db: db as any, kanban: createMockKanban(), claudeService: createMockClaudeService(),
2714
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
2715
+ });
2716
+
2717
+ // Simulate an active child process with an EventEmitter-like on()
2718
+ const mockChild = {
2719
+ kill: mock.fn(() => true),
2720
+ on: mock.fn((_event: string, cb: () => void) => { cb(); }), // immediately emit 'close'
2721
+ };
2722
+ (engine as any).activeProcesses.set(execId, mockChild);
2723
+
2724
+ await engine.continueNode('feat_test', 'agent_dev');
2725
+
2726
+ // The existing process should have been killed with SIGTERM
2727
+ assert.equal(mockChild.kill.mock.calls.length, 1);
2728
+ assert.equal(mockChild.kill.mock.calls[0].arguments[0], 'SIGTERM');
2729
+ // The process should be removed from activeProcesses (before the resumed one is added)
2730
+ // stoppedExecutions should be cleaned up (no lingering entries)
2731
+ assert.ok(!(engine as any).stoppedExecutions.has(execId));
2732
+ });
2733
+
2734
+ it('picks the highest-attempt node execution record', async () => {
2735
+ const db = createMockDb();
2736
+ const execId = 'exec-cont-7';
2737
+ const nodeExecId1 = 'ne-cont-7a';
2738
+ const nodeExecId2 = 'ne-cont-7b';
2739
+ const sessionId = 'session-latest';
2740
+
2741
+ db._stores.executionsStore[execId] = {
2742
+ id: execId, workflowId: WORKFLOW_ID, featureId: 'feat_test',
2743
+ status: 'failed', currentNodeId: 'agent_dev',
2744
+ context: JSON.stringify(defaultContext()), startedAt: '2026-01-01', updatedAt: '2026-01-01',
2745
+ };
2746
+ // Attempt 1 — completed previously
2747
+ db._stores.nodeExecutionsStore[nodeExecId1] = {
2748
+ id: nodeExecId1, executionId: execId, nodeId: 'agent_dev',
2749
+ status: 'completed', startedAt: '2026-01-01', completedAt: '2026-01-01',
2750
+ outputData: null, error: null, attempt: 1, claudeSessionId: 'session-old',
2751
+ };
2752
+ // Attempt 2 — the one that failed and should be continued
2753
+ db._stores.nodeExecutionsStore[nodeExecId2] = {
2754
+ id: nodeExecId2, executionId: execId, nodeId: 'agent_dev',
2755
+ status: 'failed', startedAt: '2026-01-01', completedAt: null,
2756
+ outputData: null, error: 'OOM', attempt: 2, claudeSessionId: sessionId,
2757
+ };
2758
+
2759
+ const claudeService = createMockClaudeService();
2760
+ const engine = new WorkflowEngine({
2761
+ db: db as any, kanban: createMockKanban(), claudeService,
2762
+ gitWorkflow: createMockGitWorkflow(), skillsDir: TEST_SKILLS_DIR, log: createMockLog(),
2763
+ });
2764
+
2765
+ await engine.continueNode('feat_test', 'agent_dev');
2766
+ await new Promise(r => setTimeout(r, 50));
2767
+
2768
+ // Should resume the latest session (attempt 2), not attempt 1
2769
+ const call = claudeService.spawnClaude.mock.calls[0];
2770
+ assert.equal(call.arguments[3].resume, sessionId);
2771
+ // Attempt should still be 2 — not incremented (continuation, not retry)
2772
+ assert.equal(db._stores.nodeExecutionsStore[nodeExecId2].attempt, 2);
2773
+ });
2774
+ });
1999
2775
  });