@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.
- package/dist/bin/create.js +0 -0
- package/package.json +9 -7
- package/templates/assistkick-product-system/.env.example +1 -0
- package/templates/assistkick-product-system/local.db +0 -0
- package/templates/assistkick-product-system/package.json +4 -2
- package/templates/assistkick-product-system/packages/backend/package.json +2 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
- package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +61 -6
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
- package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +158 -0
- package/templates/assistkick-product-system/packages/backend/src/server.ts +60 -9
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
- package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
- package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +43 -77
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
- package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +245 -0
- package/templates/assistkick-product-system/packages/frontend/package-lock.json +3455 -0
- package/templates/assistkick-product-system/packages/frontend/package.json +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +8 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +458 -18
- package/templates/assistkick-product-system/packages/frontend/src/api/client_files.test.ts +172 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client_video.test.ts +238 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +307 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/CoherenceView.tsx +82 -66
- package/templates/assistkick-product-system/packages/frontend/src/components/CompositionPlaceholder.tsx +97 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +20 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +57 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +313 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeContextMenu.tsx +61 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +73 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +404 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +187 -56
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +71 -73
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphSettings.tsx +8 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphView.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/InviteUserDialog.tsx +15 -11
- package/templates/assistkick-product-system/packages/frontend/src/components/IterationCommentModal.tsx +80 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +263 -167
- package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
- package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +32 -49
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +43 -48
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +20 -14
- package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
- package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +103 -87
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +9 -188
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +81 -37
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GenerateTTSNode.tsx +52 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RebuildBundleNode.tsx +20 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RenderVideoNode.tsx +72 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +341 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +643 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +246 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +136 -0
- package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
- package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
- package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
- package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
- package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
- package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +1 -1
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +6 -3
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +4 -4
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +275 -3535
- package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
- package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
- package/templates/assistkick-product-system/packages/frontend/vite.config.ts +5 -0
- package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0014_nifty_punisher.sql +15 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0014_snapshot.json +1545 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +77 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +114 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
- package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
- package/templates/assistkick-product-system/packages/shared/lib/graph.ts +5 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1999 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1437 -0
- package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
- package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
- package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
- package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
- package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
- package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
- package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
- package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
- package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
- package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
- package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
- package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
- package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
- package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
- package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
- package/templates/assistkick-product-system/packages/video/index.ts +4 -0
- package/templates/assistkick-product-system/packages/video/package.json +28 -0
- package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
- package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
- package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
- package/templates/assistkick-product-system/packages/video/style.css +1 -0
- package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
- package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
- package/templates/assistkick-product-system/tests/video_render_service.test.ts +181 -0
- package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
- package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
- package/templates/skills/assistkick-developer/SKILL.md +3 -0
- package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
- package/templates/skills/product-system/graph.json +1890 -0
- package/templates/skills/product-system/kanban.json +304 -0
- package/templates/skills/product-system/nodes/comp_001.md +56 -0
- package/templates/skills/product-system/nodes/comp_002.md +57 -0
- package/templates/skills/product-system/nodes/data_001.md +51 -0
- package/templates/skills/product-system/nodes/data_002.md +40 -0
- package/templates/skills/product-system/nodes/data_004.md +38 -0
- package/templates/skills/product-system/nodes/dec_001.md +34 -0
- package/templates/skills/product-system/nodes/dec_016.md +32 -0
- package/templates/skills/product-system/nodes/feat_008.md +30 -0
- package/templates/skills/video-composition-agent/SKILL.md +232 -0
- 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
|
-
//
|
|
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
|
+
});
|