@assistkick/create 1.7.0 → 1.9.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 (206) hide show
  1. package/dist/bin/create.js +0 -0
  2. package/package.json +9 -7
  3. package/templates/assistkick-product-system/.env.example +1 -0
  4. package/templates/assistkick-product-system/local.db +0 -0
  5. package/templates/assistkick-product-system/package.json +4 -2
  6. package/templates/assistkick-product-system/packages/backend/package.json +2 -0
  7. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
  9. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
  10. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
  11. package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
  12. package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +61 -6
  13. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
  14. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
  15. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
  16. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
  17. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
  18. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +158 -0
  19. package/templates/assistkick-product-system/packages/backend/src/server.ts +60 -9
  20. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
  21. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
  22. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
  23. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
  24. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +43 -77
  25. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
  26. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
  27. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
  28. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
  29. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
  30. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
  31. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
  32. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
  33. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
  34. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
  35. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
  40. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +245 -0
  41. package/templates/assistkick-product-system/packages/frontend/package-lock.json +3455 -0
  42. package/templates/assistkick-product-system/packages/frontend/package.json +6 -0
  43. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +8 -0
  44. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +458 -18
  45. package/templates/assistkick-product-system/packages/frontend/src/api/client_files.test.ts +172 -0
  46. package/templates/assistkick-product-system/packages/frontend/src/api/client_video.test.ts +238 -0
  47. package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +307 -0
  48. package/templates/assistkick-product-system/packages/frontend/src/components/CoherenceView.tsx +82 -66
  49. package/templates/assistkick-product-system/packages/frontend/src/components/CompositionPlaceholder.tsx +97 -0
  50. package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +20 -0
  51. package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +57 -0
  52. package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +313 -0
  53. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeContextMenu.tsx +61 -0
  54. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +73 -0
  55. package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +404 -0
  56. package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +187 -56
  57. package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +71 -73
  58. package/templates/assistkick-product-system/packages/frontend/src/components/GraphSettings.tsx +8 -8
  59. package/templates/assistkick-product-system/packages/frontend/src/components/GraphView.tsx +1 -1
  60. package/templates/assistkick-product-system/packages/frontend/src/components/InviteUserDialog.tsx +15 -11
  61. package/templates/assistkick-product-system/packages/frontend/src/components/IterationCommentModal.tsx +80 -0
  62. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +263 -167
  63. package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
  64. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
  65. package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +32 -49
  66. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +43 -48
  67. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
  68. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +20 -14
  69. package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
  70. package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
  71. package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +103 -87
  82. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +9 -188
  83. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
  85. package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +81 -37
  86. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
  87. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
  88. package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
  89. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
  92. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GenerateTTSNode.tsx +52 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RebuildBundleNode.tsx +20 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RenderVideoNode.tsx +72 -0
  97. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
  99. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +341 -0
  102. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +643 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
  105. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +246 -0
  106. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
  107. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +136 -0
  108. package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
  109. package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
  110. package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
  111. package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
  112. package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
  113. package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
  114. package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
  115. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
  116. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +1 -1
  117. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +2 -2
  118. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
  119. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -2
  120. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
  121. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +6 -3
  123. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +4 -4
  124. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +275 -3535
  125. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
  128. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
  129. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
  130. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
  131. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
  132. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
  133. package/templates/assistkick-product-system/packages/frontend/vite.config.ts +5 -0
  134. package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
  135. package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
  136. package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
  137. package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
  138. package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
  139. package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
  140. package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
  141. package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
  142. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
  143. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
  144. package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
  145. package/templates/assistkick-product-system/packages/shared/db/migrations/0014_nifty_punisher.sql +15 -0
  146. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
  147. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
  148. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
  149. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
  150. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
  151. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
  152. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
  153. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
  154. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
  155. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0014_snapshot.json +1545 -0
  156. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +77 -0
  157. package/templates/assistkick-product-system/packages/shared/db/schema.ts +114 -0
  158. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
  159. package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
  160. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
  161. package/templates/assistkick-product-system/packages/shared/lib/graph.ts +5 -0
  162. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1999 -0
  163. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1437 -0
  164. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
  165. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
  166. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
  167. package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
  168. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
  169. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
  170. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
  171. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
  172. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
  173. package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
  174. package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
  175. package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
  176. package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
  177. package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
  178. package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
  179. package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
  180. package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
  181. package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
  182. package/templates/assistkick-product-system/packages/video/index.ts +4 -0
  183. package/templates/assistkick-product-system/packages/video/package.json +28 -0
  184. package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
  185. package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
  186. package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
  187. package/templates/assistkick-product-system/packages/video/style.css +1 -0
  188. package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
  189. package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
  190. package/templates/assistkick-product-system/tests/video_render_service.test.ts +181 -0
  191. package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
  192. package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
  193. package/templates/skills/assistkick-developer/SKILL.md +3 -0
  194. package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
  195. package/templates/skills/product-system/graph.json +1890 -0
  196. package/templates/skills/product-system/kanban.json +304 -0
  197. package/templates/skills/product-system/nodes/comp_001.md +56 -0
  198. package/templates/skills/product-system/nodes/comp_002.md +57 -0
  199. package/templates/skills/product-system/nodes/data_001.md +51 -0
  200. package/templates/skills/product-system/nodes/data_002.md +40 -0
  201. package/templates/skills/product-system/nodes/data_004.md +38 -0
  202. package/templates/skills/product-system/nodes/dec_001.md +34 -0
  203. package/templates/skills/product-system/nodes/dec_016.md +32 -0
  204. package/templates/skills/product-system/nodes/feat_008.md +30 -0
  205. package/templates/skills/video-composition-agent/SKILL.md +232 -0
  206. package/templates/skills/video-script-writer/SKILL.md +136 -0
@@ -0,0 +1,211 @@
1
+ /**
2
+ * WorkflowOrchestrator — runs the Play All loop server-side using WorkflowEngine.
3
+ * Iterates through TODO features sequentially, respecting dependency ordering.
4
+ * Continues across browser sessions. On server restart, does NOT auto-resume.
5
+ */
6
+
7
+ import type { WorkflowEngine } from './workflow_engine.js';
8
+
9
+ interface WorkflowOrchestratorDeps {
10
+ workflowEngine: WorkflowEngine;
11
+ loadKanban: (projectId?: string) => Promise<any>;
12
+ readGraph: (projectId?: string) => Promise<any>;
13
+ resolveDefaultWorkflow: (projectId?: string) => Promise<{ id: string } | null>;
14
+ log: (tag: string, ...args: any[]) => void;
15
+ }
16
+
17
+ export class WorkflowOrchestrator {
18
+ private workflowEngine: WorkflowEngine;
19
+ private loadKanban: (projectId?: string) => Promise<any>;
20
+ private readGraph: (projectId?: string) => Promise<any>;
21
+ private resolveDefaultWorkflow: (projectId?: string) => Promise<{ id: string } | null>;
22
+ private log: (tag: string, ...args: any[]) => void;
23
+
24
+ private active = false;
25
+ private aborted = false;
26
+ private currentFeatureId: string | null = null;
27
+ private projectId: string = '';
28
+ private epicId: string | null = null;
29
+ private processedCount = 0;
30
+ private skippedFeatures: string[] = [];
31
+
32
+ constructor({ workflowEngine, loadKanban, readGraph, resolveDefaultWorkflow, log }: WorkflowOrchestratorDeps) {
33
+ this.workflowEngine = workflowEngine;
34
+ this.loadKanban = loadKanban;
35
+ this.readGraph = readGraph;
36
+ this.resolveDefaultWorkflow = resolveDefaultWorkflow;
37
+ this.log = log;
38
+ }
39
+
40
+ startPlayAll = async (projectId: string, epicId?: string) => {
41
+ if (this.active) {
42
+ return { error: 'Play All is already running', status: 409 };
43
+ }
44
+
45
+ this.active = true;
46
+ this.aborted = false;
47
+ this.currentFeatureId = null;
48
+ this.projectId = projectId;
49
+ this.epicId = epicId || null;
50
+ this.processedCount = 0;
51
+ this.skippedFeatures = [];
52
+
53
+ const scope = epicId ? ` for epic ${epicId}` : '';
54
+ this.log('ORCHESTRATOR', `Play All started${projectId ? ` for project ${projectId}` : ''}${scope}`);
55
+
56
+ // Fire-and-forget — loop runs in the background
57
+ this.runLoop().catch(err => {
58
+ this.log('ORCHESTRATOR', `Play All UNCAUGHT ERROR: ${err.message}`);
59
+ }).finally(() => {
60
+ this.active = false;
61
+ this.currentFeatureId = null;
62
+ this.log('ORCHESTRATOR', `Play All ended. Processed ${this.processedCount} features.`);
63
+ });
64
+
65
+ return { started: true, status: 200 };
66
+ };
67
+
68
+ stopPlayAll = () => {
69
+ if (!this.active) {
70
+ return { error: 'Play All is not running', status: 400 };
71
+ }
72
+ this.aborted = true;
73
+ this.log('ORCHESTRATOR', 'Play All stop requested');
74
+ return { stopped: true, status: 200 };
75
+ };
76
+
77
+ getStatus = () => {
78
+ return {
79
+ active: this.active,
80
+ currentFeatureId: this.currentFeatureId,
81
+ projectId: this.projectId,
82
+ epicId: this.epicId,
83
+ processedCount: this.processedCount,
84
+ skippedFeatures: this.skippedFeatures,
85
+ };
86
+ };
87
+
88
+ private runLoop = async () => {
89
+ // Resolve default workflow once at the start
90
+ const defaultWorkflow = await this.resolveDefaultWorkflow(this.projectId);
91
+ if (!defaultWorkflow) {
92
+ this.log('ORCHESTRATOR', 'No default workflow found — cannot start Play All');
93
+ return;
94
+ }
95
+
96
+ while (!this.aborted) {
97
+ let kanbanData: any;
98
+ let graphData: any;
99
+ try {
100
+ kanbanData = await this.loadKanban(this.projectId);
101
+ graphData = await this.readGraph(this.projectId);
102
+ } catch (err: any) {
103
+ this.log('ORCHESTRATOR', `Failed to fetch data: ${err.message}`);
104
+ break;
105
+ }
106
+
107
+ // Build feature node lookup
108
+ const featureNodes = new Map<string, any>();
109
+ graphData.nodes
110
+ .filter((n: any) => n.type === 'feature')
111
+ .forEach((n: any) => featureNodes.set(n.id, n));
112
+
113
+ // When running for a specific epic, restrict to its contained features
114
+ if (this.epicId) {
115
+ const epicChildIds = new Set<string>(
116
+ (graphData.edges || [])
117
+ .filter((e: any) => e.from === this.epicId && e.relation === 'contains')
118
+ .map((e: any) => e.to),
119
+ );
120
+ for (const id of featureNodes.keys()) {
121
+ if (!epicChildIds.has(id)) featureNodes.delete(id);
122
+ }
123
+ }
124
+
125
+ // Get TODO cards that aren't dev_blocked, sorted by completeness (highest first)
126
+ const todoCards = Object.entries(kanbanData)
127
+ .filter(([id, entry]: [string, any]) =>
128
+ entry.column === 'todo' && featureNodes.has(id) && !entry.dev_blocked
129
+ )
130
+ .map(([id]: [string, any]) => ({
131
+ id,
132
+ completeness: featureNodes.get(id).completeness || 0,
133
+ }))
134
+ .sort((a, b) => b.completeness - a.completeness);
135
+
136
+ if (todoCards.length === 0) {
137
+ this.log('ORCHESTRATOR', 'No TODO cards remaining — stopping');
138
+ break;
139
+ }
140
+
141
+ // Try to find an unblocked card
142
+ let processed = false;
143
+ this.skippedFeatures = [];
144
+
145
+ for (const card of todoCards) {
146
+ if (this.aborted) return;
147
+
148
+ // Check dependency ordering
149
+ const deps = graphData.edges
150
+ .filter((e: any) => e.from === card.id && e.relation === 'depends_on')
151
+ .map((e: any) => e.to)
152
+ .filter((depId: string) => graphData.nodes.some((n: any) => n.id === depId && n.type === 'feature'));
153
+ const COMPLETED_COLUMNS = ['done', 'qa'];
154
+ const blocked = deps.some((depId: string) =>
155
+ !kanbanData[depId] || !COMPLETED_COLUMNS.includes(kanbanData[depId].column)
156
+ );
157
+
158
+ if (blocked) {
159
+ this.skippedFeatures.push(card.id);
160
+ continue;
161
+ }
162
+
163
+ // Start workflow for this card
164
+ this.currentFeatureId = card.id;
165
+ this.log('ORCHESTRATOR', `Starting workflow for ${card.id}`);
166
+
167
+ try {
168
+ await this.workflowEngine.start(card.id, defaultWorkflow.id, this.projectId);
169
+ } catch (err: any) {
170
+ this.log('ORCHESTRATOR', `Failed to start workflow for ${card.id}: ${err.message}`);
171
+ continue;
172
+ }
173
+
174
+ // Wait for workflow to reach a terminal status
175
+ await this.waitForWorkflowCompletion(card.id);
176
+
177
+ if (this.aborted) return;
178
+
179
+ this.processedCount++;
180
+ processed = true;
181
+ break; // Process one card per iteration, then re-fetch data
182
+ }
183
+
184
+ // Deadlock: a full pass with zero features processable
185
+ if (!processed) {
186
+ this.log('ORCHESTRATOR', `Deadlock detected — ${this.skippedFeatures.length} features blocked: ${this.skippedFeatures.join(', ')}`);
187
+ break;
188
+ }
189
+ }
190
+ };
191
+
192
+ private waitForWorkflowCompletion = async (featureId: string): Promise<void> => {
193
+ const TERMINAL_STATUSES = ['idle', 'completed', 'failed'];
194
+ const POLL_INTERVAL_MS = 5000;
195
+
196
+ while (!this.aborted) {
197
+ try {
198
+ const status = await this.workflowEngine.getStatus(featureId);
199
+ if (TERMINAL_STATUSES.includes(status.status as string)) {
200
+ this.log('ORCHESTRATOR', `Workflow for ${featureId} reached terminal status: ${status.status}`);
201
+ return;
202
+ }
203
+ } catch (err: any) {
204
+ this.log('ORCHESTRATOR', `Error polling status for ${featureId}: ${err.message}`);
205
+ return;
206
+ }
207
+
208
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
209
+ }
210
+ };
211
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ /**
5
+ * Re-implement the featureType derivation logic from add_node.ts for testing.
6
+ * This is the same logic used in the add_node script — tested here
7
+ * to verify data transformation without requiring a database connection.
8
+ */
9
+ const deriveFeatureType = (
10
+ nodeType: string,
11
+ projectType: string | null | undefined,
12
+ ): string | null => {
13
+ if (nodeType === 'feature' && projectType === 'video') {
14
+ return 'video';
15
+ }
16
+ return null;
17
+ };
18
+
19
+ describe('deriveFeatureType', () => {
20
+ it('returns "video" for feature nodes in video projects', () => {
21
+ assert.equal(deriveFeatureType('feature', 'video'), 'video');
22
+ });
23
+
24
+ it('returns null for feature nodes in software projects', () => {
25
+ assert.equal(deriveFeatureType('feature', 'software'), null);
26
+ });
27
+
28
+ it('returns null for non-feature nodes in video projects', () => {
29
+ assert.equal(deriveFeatureType('component', 'video'), null);
30
+ assert.equal(deriveFeatureType('decision', 'video'), null);
31
+ assert.equal(deriveFeatureType('epic', 'video'), null);
32
+ });
33
+
34
+ it('returns null when project type is null or undefined', () => {
35
+ assert.equal(deriveFeatureType('feature', null), null);
36
+ assert.equal(deriveFeatureType('feature', undefined), null);
37
+ });
38
+
39
+ it('returns null for non-feature nodes in software projects', () => {
40
+ assert.equal(deriveFeatureType('component', 'software'), null);
41
+ assert.equal(deriveFeatureType('decision', 'software'), null);
42
+ });
43
+ });
@@ -8,9 +8,10 @@
8
8
  import { program } from 'commander';
9
9
  import chalk from 'chalk';
10
10
  import { randomBytes } from 'node:crypto';
11
+ import { eq } from 'drizzle-orm';
11
12
  import { NODE_TYPES, VALID_FEATURE_KINDS } from '../lib/constants.js';
12
13
  import { getDb } from '../lib/db.js';
13
- import { nodes } from '../db/schema.js';
14
+ import { nodes, projects } from '../db/schema.js';
14
15
  import { deriveMetadata, templateSections } from '../lib/markdown.js';
15
16
  import { assertValidType, assertUniqueName } from '../lib/validator.js';
16
17
 
@@ -68,8 +69,17 @@ const opts = program.opts();
68
69
  })
69
70
  .join('\n');
70
71
 
71
- // Insert directly into DB
72
+ // Derive featureType from project type for feature nodes
72
73
  const db = getDb();
74
+ let featureType: string | null = null;
75
+ if (opts.type === 'feature' && opts.projectId) {
76
+ const [project] = await db.select({ type: projects.type }).from(projects).where(eq(projects.id, opts.projectId));
77
+ if (project?.type === 'video') {
78
+ featureType = 'video';
79
+ }
80
+ }
81
+
82
+ // Insert directly into DB
73
83
  await db.insert(nodes).values({
74
84
  id,
75
85
  type: opts.type,
@@ -79,6 +89,7 @@ const opts = program.opts();
79
89
  completeness: meta.completeness,
80
90
  openQuestionsCount: meta.open_questions_count,
81
91
  kind: opts.type === 'feature' ? opts.kind : null,
92
+ featureType,
82
93
  createdAt: now,
83
94
  updatedAt: now,
84
95
  body,
@@ -51,7 +51,7 @@ const opts = program.opts();
51
51
  const kindMap = {};
52
52
  for (const node of graph.nodes) {
53
53
  nameMap[node.id] = node.name;
54
- if (node.type === 'feature') {
54
+ if (node.type === 'feature' || node.type === 'epic') {
55
55
  kindMap[node.id] = node.kind || 'new';
56
56
  }
57
57
  }
@@ -0,0 +1,226 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+
4
+ // Re-implement the pure functions from migrate_epics.ts for testing.
5
+ // These are the same functions used in the migration script — tested here
6
+ // to verify data transformation logic without requiring a database connection.
7
+
8
+ const parseSections = (body: string): Record<string, string> => {
9
+ const sections: Record<string, string> = {};
10
+ const lines = body.split('\n');
11
+ let currentSection: string | null = null;
12
+ let currentLines: string[] = [];
13
+
14
+ for (const line of lines) {
15
+ const match = line.match(/^## (.+)$/);
16
+ if (match) {
17
+ if (currentSection !== null) {
18
+ sections[currentSection] = currentLines.join('\n').trim();
19
+ }
20
+ currentSection = match[1].trim();
21
+ currentLines = [];
22
+ } else if (currentSection !== null) {
23
+ currentLines.push(line);
24
+ }
25
+ }
26
+ if (currentSection !== null) {
27
+ sections[currentSection] = currentLines.join('\n').trim();
28
+ }
29
+
30
+ return sections;
31
+ };
32
+
33
+ const squishSections = (sections: Record<string, string>): string => {
34
+ const parts: string[] = [];
35
+
36
+ for (const [name, content] of Object.entries(sections)) {
37
+ if (name === 'Relations') continue;
38
+ if (!content.trim()) continue;
39
+ parts.push(`### ${name}`);
40
+ parts.push(content.trim());
41
+ parts.push('');
42
+ }
43
+
44
+ return parts.join('\n').trim();
45
+ };
46
+
47
+ const toEpicId = (featId: string): string => {
48
+ return featId.replace(/^feat_/, 'epic_');
49
+ };
50
+
51
+ const buildEpicBody = (featureBody: string): string => {
52
+ const sections = parseSections(featureBody);
53
+ const squished = squishSections(sections);
54
+ return `## Description\n${squished}\n\n## Scope\n`;
55
+ };
56
+
57
+ // ── Tests ──
58
+
59
+ describe('toEpicId', () => {
60
+ it('converts feat_ prefix to epic_', () => {
61
+ assert.equal(toEpicId('feat_a20d3b2d'), 'epic_a20d3b2d');
62
+ });
63
+
64
+ it('preserves hex suffix exactly', () => {
65
+ assert.equal(toEpicId('feat_21ad113d'), 'epic_21ad113d');
66
+ });
67
+
68
+ it('does not alter IDs without feat_ prefix', () => {
69
+ assert.equal(toEpicId('comp_abc12345'), 'comp_abc12345');
70
+ });
71
+ });
72
+
73
+ describe('parseSections', () => {
74
+ it('parses multiple sections from markdown body', () => {
75
+ const body = '## Description\nSome description text\n\n## Acceptance Criteria\n- Criterion 1\n- Criterion 2\n\n## Open Questions\n';
76
+ const sections = parseSections(body);
77
+
78
+ assert.equal(sections['Description'], 'Some description text');
79
+ assert.equal(sections['Acceptance Criteria'], '- Criterion 1\n- Criterion 2');
80
+ assert.equal(sections['Open Questions'], '');
81
+ });
82
+
83
+ it('handles empty body', () => {
84
+ const sections = parseSections('');
85
+ assert.deepEqual(sections, {});
86
+ });
87
+
88
+ it('handles body with no sections', () => {
89
+ const sections = parseSections('Just some text without headers');
90
+ assert.deepEqual(sections, {});
91
+ });
92
+
93
+ it('preserves multiline content within a section', () => {
94
+ const body = '## Notes\nLine 1\nLine 2\nLine 3';
95
+ const sections = parseSections(body);
96
+ assert.equal(sections['Notes'], 'Line 1\nLine 2\nLine 3');
97
+ });
98
+ });
99
+
100
+ describe('squishSections', () => {
101
+ it('converts section headers to ### sub-headings', () => {
102
+ const sections = {
103
+ 'Description': 'Some description',
104
+ 'Acceptance Criteria': '- AC 1\n- AC 2',
105
+ };
106
+ const result = squishSections(sections);
107
+
108
+ assert.ok(result.includes('### Description'));
109
+ assert.ok(result.includes('Some description'));
110
+ assert.ok(result.includes('### Acceptance Criteria'));
111
+ assert.ok(result.includes('- AC 1\n- AC 2'));
112
+ });
113
+
114
+ it('skips empty sections', () => {
115
+ const sections = {
116
+ 'Description': 'Some description',
117
+ 'Open Questions': '',
118
+ 'Notes': '',
119
+ };
120
+ const result = squishSections(sections);
121
+
122
+ assert.ok(result.includes('### Description'));
123
+ assert.ok(!result.includes('### Open Questions'));
124
+ assert.ok(!result.includes('### Notes'));
125
+ });
126
+
127
+ it('skips the Relations section', () => {
128
+ const sections = {
129
+ 'Description': 'Some description',
130
+ 'Relations': '- contains → feat_001',
131
+ };
132
+ const result = squishSections(sections);
133
+
134
+ assert.ok(result.includes('### Description'));
135
+ assert.ok(!result.includes('### Relations'));
136
+ assert.ok(!result.includes('feat_001'));
137
+ });
138
+
139
+ it('returns empty string when all sections are empty', () => {
140
+ const sections = {
141
+ 'Open Questions': '',
142
+ 'Notes': '',
143
+ };
144
+ const result = squishSections(sections);
145
+ assert.equal(result, '');
146
+ });
147
+ });
148
+
149
+ describe('buildEpicBody', () => {
150
+ it('creates epic body with Description and Scope sections', () => {
151
+ const featureBody = '## Description\nVideo generation system\n\n## Acceptance Criteria\n- AC 1\n- AC 2\n\n## Open Questions\n\n## Resolved Questions\n- [x] Question 1 → Answer 1\n\n## Notes\nSome notes here\n';
152
+
153
+ const epicBody = buildEpicBody(featureBody);
154
+
155
+ // Should start with ## Description
156
+ assert.ok(epicBody.startsWith('## Description\n'));
157
+ // Should end with ## Scope section
158
+ assert.ok(epicBody.includes('## Scope\n'));
159
+ // Should contain original Description as ### sub-heading
160
+ assert.ok(epicBody.includes('### Description'));
161
+ assert.ok(epicBody.includes('Video generation system'));
162
+ // Should contain Acceptance Criteria as ### sub-heading
163
+ assert.ok(epicBody.includes('### Acceptance Criteria'));
164
+ assert.ok(epicBody.includes('- AC 1'));
165
+ // Should skip empty Open Questions section
166
+ assert.ok(!epicBody.includes('### Open Questions'));
167
+ // Should include Resolved Questions
168
+ assert.ok(epicBody.includes('### Resolved Questions'));
169
+ // Should include Notes
170
+ assert.ok(epicBody.includes('### Notes'));
171
+ assert.ok(epicBody.includes('Some notes here'));
172
+ });
173
+
174
+ it('handles feature with only Description', () => {
175
+ const featureBody = '## Description\nJust a description\n';
176
+
177
+ const epicBody = buildEpicBody(featureBody);
178
+
179
+ assert.ok(epicBody.startsWith('## Description\n'));
180
+ assert.ok(epicBody.includes('### Description'));
181
+ assert.ok(epicBody.includes('Just a description'));
182
+ assert.ok(epicBody.includes('## Scope\n'));
183
+ });
184
+
185
+ it('handles feature with empty body', () => {
186
+ const epicBody = buildEpicBody('');
187
+
188
+ assert.equal(epicBody, '## Description\n\n\n## Scope\n');
189
+ });
190
+ });
191
+
192
+ describe('reviewMeta key transformation', () => {
193
+ const toMetaKey = (nodeId: string) => `kanban:${nodeId}`;
194
+
195
+ it('transforms feat_ meta key to epic_ meta key', () => {
196
+ const oldKey = toMetaKey('feat_a20d3b2d');
197
+ const newKey = toMetaKey(toEpicId('feat_a20d3b2d'));
198
+ assert.equal(oldKey, 'kanban:feat_a20d3b2d');
199
+ assert.equal(newKey, 'kanban:epic_a20d3b2d');
200
+ });
201
+
202
+ it('preserves key format after transformation', () => {
203
+ const newKey = toMetaKey(toEpicId('feat_21ad113d'));
204
+ assert.ok(newKey.startsWith('kanban:epic_'));
205
+ });
206
+ });
207
+
208
+ describe('TARGET_FEAT_IDS mapping', () => {
209
+ const targets = ['feat_a20d3b2d', 'feat_21ad113d', 'feat_e75c9f47', 'feat_c4d0e19b'];
210
+
211
+ it('maps all 4 target IDs to epic_ prefix', () => {
212
+ const mapped = targets.map(toEpicId);
213
+ assert.deepEqual(mapped, [
214
+ 'epic_a20d3b2d',
215
+ 'epic_21ad113d',
216
+ 'epic_e75c9f47',
217
+ 'epic_c4d0e19b',
218
+ ]);
219
+ });
220
+
221
+ it('produces unique epic IDs', () => {
222
+ const mapped = targets.map(toEpicId);
223
+ const unique = new Set(mapped);
224
+ assert.equal(unique.size, 4);
225
+ });
226
+ });