red64-cli 0.1.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/README.md +454 -0
- package/dist/cli/parseArgs.d.ts +16 -0
- package/dist/cli/parseArgs.d.ts.map +1 -0
- package/dist/cli/parseArgs.js +172 -0
- package/dist/cli/parseArgs.js.map +1 -0
- package/dist/cli/validateFlags.d.ts +22 -0
- package/dist/cli/validateFlags.d.ts.map +1 -0
- package/dist/cli/validateFlags.js +24 -0
- package/dist/cli/validateFlags.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +90 -0
- package/dist/cli.js.map +1 -0
- package/dist/components/App.d.ts +20 -0
- package/dist/components/App.d.ts.map +1 -0
- package/dist/components/App.js +35 -0
- package/dist/components/App.js.map +1 -0
- package/dist/components/CommandRouter.d.ts +21 -0
- package/dist/components/CommandRouter.d.ts.map +1 -0
- package/dist/components/CommandRouter.js +30 -0
- package/dist/components/CommandRouter.js.map +1 -0
- package/dist/components/GlobalConfig.d.ts +26 -0
- package/dist/components/GlobalConfig.d.ts.map +1 -0
- package/dist/components/GlobalConfig.js +30 -0
- package/dist/components/GlobalConfig.js.map +1 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +9 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/init/CompleteStep.d.ts +11 -0
- package/dist/components/init/CompleteStep.d.ts.map +1 -0
- package/dist/components/init/CompleteStep.js +15 -0
- package/dist/components/init/CompleteStep.js.map +1 -0
- package/dist/components/init/ErrorStep.d.ts +14 -0
- package/dist/components/init/ErrorStep.d.ts.map +1 -0
- package/dist/components/init/ErrorStep.js +36 -0
- package/dist/components/init/ErrorStep.js.map +1 -0
- package/dist/components/init/FetchStep.d.ts +15 -0
- package/dist/components/init/FetchStep.d.ts.map +1 -0
- package/dist/components/init/FetchStep.js +33 -0
- package/dist/components/init/FetchStep.js.map +1 -0
- package/dist/components/init/SetupStep.d.ts +14 -0
- package/dist/components/init/SetupStep.d.ts.map +1 -0
- package/dist/components/init/SetupStep.js +78 -0
- package/dist/components/init/SetupStep.js.map +1 -0
- package/dist/components/init/SteeringStep.d.ts +12 -0
- package/dist/components/init/SteeringStep.d.ts.map +1 -0
- package/dist/components/init/SteeringStep.js +43 -0
- package/dist/components/init/SteeringStep.js.map +1 -0
- package/dist/components/init/WelcomeStep.d.ts +12 -0
- package/dist/components/init/WelcomeStep.d.ts.map +1 -0
- package/dist/components/init/WelcomeStep.js +52 -0
- package/dist/components/init/WelcomeStep.js.map +1 -0
- package/dist/components/init/index.d.ts +11 -0
- package/dist/components/init/index.d.ts.map +1 -0
- package/dist/components/init/index.js +10 -0
- package/dist/components/init/index.js.map +1 -0
- package/dist/components/init/types.d.ts +98 -0
- package/dist/components/init/types.d.ts.map +1 -0
- package/dist/components/init/types.js +6 -0
- package/dist/components/init/types.js.map +1 -0
- package/dist/components/screens/AbortScreen.d.ts +14 -0
- package/dist/components/screens/AbortScreen.d.ts.map +1 -0
- package/dist/components/screens/AbortScreen.js +181 -0
- package/dist/components/screens/AbortScreen.js.map +1 -0
- package/dist/components/screens/ApprovalScreen.d.ts +24 -0
- package/dist/components/screens/ApprovalScreen.d.ts.map +1 -0
- package/dist/components/screens/ApprovalScreen.js +82 -0
- package/dist/components/screens/ApprovalScreen.js.map +1 -0
- package/dist/components/screens/HelpScreen.d.ts +20 -0
- package/dist/components/screens/HelpScreen.d.ts.map +1 -0
- package/dist/components/screens/HelpScreen.js +70 -0
- package/dist/components/screens/HelpScreen.js.map +1 -0
- package/dist/components/screens/InitScreen.d.ts +15 -0
- package/dist/components/screens/InitScreen.d.ts.map +1 -0
- package/dist/components/screens/InitScreen.js +420 -0
- package/dist/components/screens/InitScreen.js.map +1 -0
- package/dist/components/screens/ListScreen.d.ts +14 -0
- package/dist/components/screens/ListScreen.d.ts.map +1 -0
- package/dist/components/screens/ListScreen.js +57 -0
- package/dist/components/screens/ListScreen.js.map +1 -0
- package/dist/components/screens/ProgressScreen.d.ts +26 -0
- package/dist/components/screens/ProgressScreen.d.ts.map +1 -0
- package/dist/components/screens/ProgressScreen.js +64 -0
- package/dist/components/screens/ProgressScreen.js.map +1 -0
- package/dist/components/screens/ResumeScreen.d.ts +14 -0
- package/dist/components/screens/ResumeScreen.d.ts.map +1 -0
- package/dist/components/screens/ResumeScreen.js +108 -0
- package/dist/components/screens/ResumeScreen.js.map +1 -0
- package/dist/components/screens/ScreenProps.d.ts +12 -0
- package/dist/components/screens/ScreenProps.d.ts.map +1 -0
- package/dist/components/screens/ScreenProps.js +5 -0
- package/dist/components/screens/ScreenProps.js.map +1 -0
- package/dist/components/screens/StartScreen.d.ts +26 -0
- package/dist/components/screens/StartScreen.d.ts.map +1 -0
- package/dist/components/screens/StartScreen.js +1021 -0
- package/dist/components/screens/StartScreen.js.map +1 -0
- package/dist/components/screens/StatusScreen.d.ts +14 -0
- package/dist/components/screens/StatusScreen.d.ts.map +1 -0
- package/dist/components/screens/StatusScreen.js +115 -0
- package/dist/components/screens/StatusScreen.js.map +1 -0
- package/dist/components/screens/index.d.ts +15 -0
- package/dist/components/screens/index.d.ts.map +1 -0
- package/dist/components/screens/index.js +12 -0
- package/dist/components/screens/index.js.map +1 -0
- package/dist/components/ui/ErrorBoundary.d.ts +34 -0
- package/dist/components/ui/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ui/ErrorBoundary.js +37 -0
- package/dist/components/ui/ErrorBoundary.js.map +1 -0
- package/dist/components/ui/ErrorDisplay.d.ts +20 -0
- package/dist/components/ui/ErrorDisplay.d.ts.map +1 -0
- package/dist/components/ui/ErrorDisplay.js +12 -0
- package/dist/components/ui/ErrorDisplay.js.map +1 -0
- package/dist/components/ui/ErrorRecoveryPrompt.d.ts +30 -0
- package/dist/components/ui/ErrorRecoveryPrompt.d.ts.map +1 -0
- package/dist/components/ui/ErrorRecoveryPrompt.js +66 -0
- package/dist/components/ui/ErrorRecoveryPrompt.js.map +1 -0
- package/dist/components/ui/FeatureSidebar.d.ts +27 -0
- package/dist/components/ui/FeatureSidebar.d.ts.map +1 -0
- package/dist/components/ui/FeatureSidebar.js +166 -0
- package/dist/components/ui/FeatureSidebar.js.map +1 -0
- package/dist/components/ui/FlowTable.d.ts +21 -0
- package/dist/components/ui/FlowTable.d.ts.map +1 -0
- package/dist/components/ui/FlowTable.js +105 -0
- package/dist/components/ui/FlowTable.js.map +1 -0
- package/dist/components/ui/Header.d.ts +20 -0
- package/dist/components/ui/Header.d.ts.map +1 -0
- package/dist/components/ui/Header.js +11 -0
- package/dist/components/ui/Header.js.map +1 -0
- package/dist/components/ui/OutputRegion.d.ts +20 -0
- package/dist/components/ui/OutputRegion.d.ts.map +1 -0
- package/dist/components/ui/OutputRegion.js +14 -0
- package/dist/components/ui/OutputRegion.js.map +1 -0
- package/dist/components/ui/PhaseProgressView.d.ts +23 -0
- package/dist/components/ui/PhaseProgressView.d.ts.map +1 -0
- package/dist/components/ui/PhaseProgressView.js +117 -0
- package/dist/components/ui/PhaseProgressView.js.map +1 -0
- package/dist/components/ui/ProgressBar.d.ts +20 -0
- package/dist/components/ui/ProgressBar.d.ts.map +1 -0
- package/dist/components/ui/ProgressBar.js +12 -0
- package/dist/components/ui/ProgressBar.js.map +1 -0
- package/dist/components/ui/SelectMenu.d.ts +27 -0
- package/dist/components/ui/SelectMenu.d.ts.map +1 -0
- package/dist/components/ui/SelectMenu.js +21 -0
- package/dist/components/ui/SelectMenu.js.map +1 -0
- package/dist/components/ui/Spinner.d.ts +18 -0
- package/dist/components/ui/Spinner.d.ts.map +1 -0
- package/dist/components/ui/Spinner.js +10 -0
- package/dist/components/ui/Spinner.js.map +1 -0
- package/dist/components/ui/StatusLine.d.ts +21 -0
- package/dist/components/ui/StatusLine.d.ts.map +1 -0
- package/dist/components/ui/StatusLine.js +30 -0
- package/dist/components/ui/StatusLine.js.map +1 -0
- package/dist/components/ui/index.d.ts +16 -0
- package/dist/components/ui/index.d.ts.map +1 -0
- package/dist/components/ui/index.js +16 -0
- package/dist/components/ui/index.js.map +1 -0
- package/dist/services/AgentInvoker.d.ts +20 -0
- package/dist/services/AgentInvoker.d.ts.map +1 -0
- package/dist/services/AgentInvoker.js +282 -0
- package/dist/services/AgentInvoker.js.map +1 -0
- package/dist/services/BranchService.d.ts +28 -0
- package/dist/services/BranchService.d.ts.map +1 -0
- package/dist/services/BranchService.js +114 -0
- package/dist/services/BranchService.js.map +1 -0
- package/dist/services/CacheService.d.ts +57 -0
- package/dist/services/CacheService.d.ts.map +1 -0
- package/dist/services/CacheService.js +208 -0
- package/dist/services/CacheService.js.map +1 -0
- package/dist/services/ClaudeErrorDetector.d.ts +45 -0
- package/dist/services/ClaudeErrorDetector.d.ts.map +1 -0
- package/dist/services/ClaudeErrorDetector.js +207 -0
- package/dist/services/ClaudeErrorDetector.js.map +1 -0
- package/dist/services/ClaudeHealthCheck.d.ts +37 -0
- package/dist/services/ClaudeHealthCheck.d.ts.map +1 -0
- package/dist/services/ClaudeHealthCheck.js +197 -0
- package/dist/services/ClaudeHealthCheck.js.map +1 -0
- package/dist/services/CommitService.d.ts +36 -0
- package/dist/services/CommitService.d.ts.map +1 -0
- package/dist/services/CommitService.js +159 -0
- package/dist/services/CommitService.js.map +1 -0
- package/dist/services/ConfigService.d.ts +49 -0
- package/dist/services/ConfigService.d.ts.map +1 -0
- package/dist/services/ConfigService.js +57 -0
- package/dist/services/ConfigService.js.map +1 -0
- package/dist/services/DockerRunner.d.ts +45 -0
- package/dist/services/DockerRunner.d.ts.map +1 -0
- package/dist/services/DockerRunner.js +170 -0
- package/dist/services/DockerRunner.js.map +1 -0
- package/dist/services/ExtendedFlowStateMachine.d.ts +31 -0
- package/dist/services/ExtendedFlowStateMachine.d.ts.map +1 -0
- package/dist/services/ExtendedFlowStateMachine.js +302 -0
- package/dist/services/ExtendedFlowStateMachine.js.map +1 -0
- package/dist/services/FeatureValidator.d.ts +26 -0
- package/dist/services/FeatureValidator.d.ts.map +1 -0
- package/dist/services/FeatureValidator.js +48 -0
- package/dist/services/FeatureValidator.js.map +1 -0
- package/dist/services/FlowStateMachine.d.ts +26 -0
- package/dist/services/FlowStateMachine.d.ts.map +1 -0
- package/dist/services/FlowStateMachine.js +177 -0
- package/dist/services/FlowStateMachine.js.map +1 -0
- package/dist/services/GitHubService.d.ts +72 -0
- package/dist/services/GitHubService.d.ts.map +1 -0
- package/dist/services/GitHubService.js +150 -0
- package/dist/services/GitHubService.js.map +1 -0
- package/dist/services/GitStatusChecker.d.ts +29 -0
- package/dist/services/GitStatusChecker.d.ts.map +1 -0
- package/dist/services/GitStatusChecker.js +127 -0
- package/dist/services/GitStatusChecker.js.map +1 -0
- package/dist/services/PRCreatorService.d.ts +59 -0
- package/dist/services/PRCreatorService.d.ts.map +1 -0
- package/dist/services/PRCreatorService.js +212 -0
- package/dist/services/PRCreatorService.js.map +1 -0
- package/dist/services/PRStatusFetcher.d.ts +39 -0
- package/dist/services/PRStatusFetcher.d.ts.map +1 -0
- package/dist/services/PRStatusFetcher.js +144 -0
- package/dist/services/PRStatusFetcher.js.map +1 -0
- package/dist/services/PhaseExecutor.d.ts +29 -0
- package/dist/services/PhaseExecutor.d.ts.map +1 -0
- package/dist/services/PhaseExecutor.js +125 -0
- package/dist/services/PhaseExecutor.js.map +1 -0
- package/dist/services/SpecInitService.d.ts +33 -0
- package/dist/services/SpecInitService.d.ts.map +1 -0
- package/dist/services/SpecInitService.js +168 -0
- package/dist/services/SpecInitService.js.map +1 -0
- package/dist/services/StateStore.d.ts +24 -0
- package/dist/services/StateStore.d.ts.map +1 -0
- package/dist/services/StateStore.js +171 -0
- package/dist/services/StateStore.js.map +1 -0
- package/dist/services/TaskParser.d.ts +44 -0
- package/dist/services/TaskParser.d.ts.map +1 -0
- package/dist/services/TaskParser.js +167 -0
- package/dist/services/TaskParser.js.map +1 -0
- package/dist/services/TaskRunner.d.ts +52 -0
- package/dist/services/TaskRunner.d.ts.map +1 -0
- package/dist/services/TaskRunner.js +135 -0
- package/dist/services/TaskRunner.js.map +1 -0
- package/dist/services/TemplateService.d.ts +73 -0
- package/dist/services/TemplateService.d.ts.map +1 -0
- package/dist/services/TemplateService.js +263 -0
- package/dist/services/TemplateService.js.map +1 -0
- package/dist/services/WorktreeService.d.ts +51 -0
- package/dist/services/WorktreeService.d.ts.map +1 -0
- package/dist/services/WorktreeService.js +204 -0
- package/dist/services/WorktreeService.js.map +1 -0
- package/dist/services/index.d.ts +25 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +25 -0
- package/dist/services/index.js.map +1 -0
- package/dist/types/extended-flow.d.ts +167 -0
- package/dist/types/extended-flow.d.ts.map +1 -0
- package/dist/types/extended-flow.js +103 -0
- package/dist/types/extended-flow.js.map +1 -0
- package/dist/types/index.d.ts +210 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +28 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/git.d.ts +41 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +68 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/paths.d.ts +30 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +43 -0
- package/dist/utils/paths.js.map +1 -0
- package/framework/.red64/settings/rules/design-discovery-full.md +93 -0
- package/framework/.red64/settings/rules/design-discovery-light.md +49 -0
- package/framework/.red64/settings/rules/design-principles.md +182 -0
- package/framework/.red64/settings/rules/design-review.md +110 -0
- package/framework/.red64/settings/rules/ears-format.md +49 -0
- package/framework/.red64/settings/rules/gap-analysis.md +144 -0
- package/framework/.red64/settings/rules/steering-principles.md +90 -0
- package/framework/.red64/settings/rules/tasks-generation.md +131 -0
- package/framework/.red64/settings/rules/tasks-parallel-analysis.md +34 -0
- package/framework/.red64/settings/templates/flow-state.json +48 -0
- package/framework/.red64/settings/templates/specs/design.md +276 -0
- package/framework/.red64/settings/templates/specs/init.json +24 -0
- package/framework/.red64/settings/templates/specs/requirements-init.md +9 -0
- package/framework/.red64/settings/templates/specs/requirements.md +26 -0
- package/framework/.red64/settings/templates/specs/research.md +61 -0
- package/framework/.red64/settings/templates/specs/tasks.md +21 -0
- package/framework/.red64/settings/templates/steering/product.md +18 -0
- package/framework/.red64/settings/templates/steering/structure.md +41 -0
- package/framework/.red64/settings/templates/steering/tech.md +45 -0
- package/framework/.red64/settings/templates/steering-custom/api-standards.md +69 -0
- package/framework/.red64/settings/templates/steering-custom/authentication.md +67 -0
- package/framework/.red64/settings/templates/steering-custom/database.md +46 -0
- package/framework/.red64/settings/templates/steering-custom/deployment.md +54 -0
- package/framework/.red64/settings/templates/steering-custom/error-handling.md +59 -0
- package/framework/.red64/settings/templates/steering-custom/security.md +55 -0
- package/framework/.red64/settings/templates/steering-custom/testing.md +47 -0
- package/framework/agents/claude/.claude/agents/red64/spec-design.md +174 -0
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +120 -0
- package/framework/agents/claude/.claude/agents/red64/spec-requirements.md +102 -0
- package/framework/agents/claude/.claude/agents/red64/spec-tasks.md +141 -0
- package/framework/agents/claude/.claude/agents/red64/steering-custom.md +147 -0
- package/framework/agents/claude/.claude/agents/red64/steering.md +163 -0
- package/framework/agents/claude/.claude/agents/red64/validate-design.md +98 -0
- package/framework/agents/claude/.claude/agents/red64/validate-gap.md +99 -0
- package/framework/agents/claude/.claude/agents/red64/validate-impl.md +146 -0
- package/framework/agents/claude/.claude/commands/red64/spec-design.md +64 -0
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +68 -0
- package/framework/agents/claude/.claude/commands/red64/spec-init.md +65 -0
- package/framework/agents/claude/.claude/commands/red64/spec-quick.md +360 -0
- package/framework/agents/claude/.claude/commands/red64/spec-requirements.md +62 -0
- package/framework/agents/claude/.claude/commands/red64/spec-status.md +87 -0
- package/framework/agents/claude/.claude/commands/red64/spec-tasks.md +75 -0
- package/framework/agents/claude/.claude/commands/red64/steering-custom.md +59 -0
- package/framework/agents/claude/.claude/commands/red64/steering.md +62 -0
- package/framework/agents/claude/.claude/commands/red64/validate-design.md +59 -0
- package/framework/agents/claude/.claude/commands/red64/validate-gap.md +53 -0
- package/framework/agents/claude/.claude/commands/red64/validate-impl.md +68 -0
- package/framework/agents/claude/docs/CLAUDE.md +45 -0
- package/framework/agents/codex/.codex/agents/red64/spec-design.md +174 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +120 -0
- package/framework/agents/codex/.codex/agents/red64/spec-requirements.md +102 -0
- package/framework/agents/codex/.codex/agents/red64/spec-tasks.md +141 -0
- package/framework/agents/codex/.codex/agents/red64/steering-custom.md +147 -0
- package/framework/agents/codex/.codex/agents/red64/steering.md +163 -0
- package/framework/agents/codex/.codex/agents/red64/validate-design.md +98 -0
- package/framework/agents/codex/.codex/agents/red64/validate-gap.md +99 -0
- package/framework/agents/codex/.codex/agents/red64/validate-impl.md +146 -0
- package/framework/agents/codex/.codex/commands/red64/spec-design.md +64 -0
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +68 -0
- package/framework/agents/codex/.codex/commands/red64/spec-init.md +65 -0
- package/framework/agents/codex/.codex/commands/red64/spec-quick.md +360 -0
- package/framework/agents/codex/.codex/commands/red64/spec-requirements.md +62 -0
- package/framework/agents/codex/.codex/commands/red64/spec-status.md +87 -0
- package/framework/agents/codex/.codex/commands/red64/spec-tasks.md +75 -0
- package/framework/agents/codex/.codex/commands/red64/steering-custom.md +59 -0
- package/framework/agents/codex/.codex/commands/red64/steering.md +62 -0
- package/framework/agents/codex/.codex/commands/red64/validate-design.md +59 -0
- package/framework/agents/codex/.codex/commands/red64/validate-gap.md +53 -0
- package/framework/agents/codex/.codex/commands/red64/validate-impl.md +68 -0
- package/framework/agents/codex/docs/AGENTS.md +68 -0
- package/framework/agents/gemini/commands.toml +607 -0
- package/framework/agents/gemini/docs/GEMINI.md +45 -0
- package/framework/stacks/generic/product.md +27 -0
- package/framework/stacks/generic/structure.md +46 -0
- package/framework/stacks/generic/tech.md +47 -0
- package/framework/stacks/node/product.md +27 -0
- package/framework/stacks/node/structure.md +82 -0
- package/framework/stacks/node/tech.md +63 -0
- package/framework/stacks/python/product.md +27 -0
- package/framework/stacks/python/structure.md +78 -0
- package/framework/stacks/python/tech.md +64 -0
- package/framework/stacks/react/product.md +27 -0
- package/framework/stacks/react/structure.md +76 -0
- package/framework/stacks/react/tech.md +65 -0
- package/package.json +47 -0
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Start screen component - orchestrates the spec-driven development flow
|
|
4
|
+
* Requirements: 4.2
|
|
5
|
+
*
|
|
6
|
+
* Flow:
|
|
7
|
+
* 1. Create git worktree for isolation
|
|
8
|
+
* 2. Initialize spec directory → commit
|
|
9
|
+
* 3. Generate requirements → commit
|
|
10
|
+
* 4. Approval gate
|
|
11
|
+
* 5. Generate design → commit
|
|
12
|
+
* 6. Approval gate
|
|
13
|
+
* 7. Generate tasks → commit
|
|
14
|
+
* 8. Approval gate
|
|
15
|
+
* 9. For each task: run spec-impl {task} → commit
|
|
16
|
+
* 10. Complete
|
|
17
|
+
*/
|
|
18
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
19
|
+
import { Box, Text, useApp } from 'ink';
|
|
20
|
+
import { Spinner, Select } from '@inkjs/ui';
|
|
21
|
+
import { createStateStore, createAgentInvoker, createExtendedFlowMachine, createWorktreeService, createCommitService, createTaskParser, createSpecInitService, createClaudeHealthCheck, createGitStatusChecker, createConfigService, sanitizeFeatureName } from '../../services/index.js';
|
|
22
|
+
import { FeatureSidebar } from '../ui/index.js';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { appendFile, mkdir } from 'node:fs/promises';
|
|
25
|
+
/**
|
|
26
|
+
* Phase display information
|
|
27
|
+
*/
|
|
28
|
+
const PHASE_LABELS = {
|
|
29
|
+
'idle': { label: 'Idle', description: 'Ready to start' },
|
|
30
|
+
'initializing': { label: 'Initializing', description: 'Setting up spec directory' },
|
|
31
|
+
'requirements-generating': { label: 'Requirements', description: 'Generating requirements' },
|
|
32
|
+
'requirements-approval': { label: 'Requirements Review', description: 'Review and approve requirements' },
|
|
33
|
+
'gap-analysis': { label: 'Gap Analysis', description: 'Analyzing existing codebase' },
|
|
34
|
+
'gap-review': { label: 'Gap Review', description: 'Review gap analysis' },
|
|
35
|
+
'design-generating': { label: 'Design', description: 'Generating technical design' },
|
|
36
|
+
'design-approval': { label: 'Design Review', description: 'Review and approve design' },
|
|
37
|
+
'design-validation': { label: 'Design Validation', description: 'Validating design' },
|
|
38
|
+
'design-validation-review': { label: 'Validation Review', description: 'Review design validation' },
|
|
39
|
+
'tasks-generating': { label: 'Tasks', description: 'Generating implementation tasks' },
|
|
40
|
+
'tasks-approval': { label: 'Tasks Review', description: 'Review and approve tasks' },
|
|
41
|
+
'implementing': { label: 'Implementing', description: 'Executing implementation tasks' },
|
|
42
|
+
'paused': { label: 'Paused', description: 'Flow paused' },
|
|
43
|
+
'validation': { label: 'Validation', description: 'Validating implementation' },
|
|
44
|
+
'pr': { label: 'Pull Request', description: 'Creating pull request' },
|
|
45
|
+
'merge-decision': { label: 'Merge Decision', description: 'Decide whether to merge' },
|
|
46
|
+
'complete': { label: 'Complete', description: 'Flow completed successfully' },
|
|
47
|
+
'aborted': { label: 'Aborted', description: 'Flow was aborted' },
|
|
48
|
+
'error': { label: 'Error', description: 'An error occurred' }
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Approval options for review phases
|
|
52
|
+
*/
|
|
53
|
+
const APPROVAL_OPTIONS = [
|
|
54
|
+
{ value: 'approve', label: 'Approve and continue' },
|
|
55
|
+
{ value: 'reject', label: 'Reject and regenerate' },
|
|
56
|
+
{ value: 'pause', label: 'Pause flow' }
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Options when existing flow is detected
|
|
60
|
+
*/
|
|
61
|
+
const EXISTING_FLOW_OPTIONS = [
|
|
62
|
+
{ value: 'resume', label: 'Resume from where you left off' },
|
|
63
|
+
{ value: 'restart', label: 'Start fresh (discard previous progress)' },
|
|
64
|
+
{ value: 'abort', label: 'Cancel' }
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* Options when uncommitted changes are detected
|
|
68
|
+
*/
|
|
69
|
+
const UNCOMMITTED_CHANGES_OPTIONS = [
|
|
70
|
+
{ value: 'commit', label: 'Commit changes (WIP) and continue' },
|
|
71
|
+
{ value: 'discard', label: 'Discard changes and continue' },
|
|
72
|
+
{ value: 'abort', label: 'Cancel' }
|
|
73
|
+
];
|
|
74
|
+
/**
|
|
75
|
+
* Human-readable labels for Claude error codes
|
|
76
|
+
*/
|
|
77
|
+
function getClaudeErrorLabel(code) {
|
|
78
|
+
const labels = {
|
|
79
|
+
CREDIT_EXHAUSTED: 'Insufficient Credits',
|
|
80
|
+
RATE_LIMITED: 'Rate Limited',
|
|
81
|
+
AUTH_FAILED: 'Authentication Failed',
|
|
82
|
+
MODEL_UNAVAILABLE: 'Service Unavailable',
|
|
83
|
+
CONTEXT_EXCEEDED: 'Context Too Large',
|
|
84
|
+
NETWORK_ERROR: 'Network Error',
|
|
85
|
+
PERMISSION_DENIED: 'Request Blocked',
|
|
86
|
+
UNKNOWN: 'Unknown Error'
|
|
87
|
+
};
|
|
88
|
+
return labels[code] ?? code;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Start screen - orchestrates the spec-driven development flow with:
|
|
92
|
+
* - Git worktree isolation
|
|
93
|
+
* - Commits after each phase
|
|
94
|
+
* - Task-by-task implementation with commits
|
|
95
|
+
*/
|
|
96
|
+
export const StartScreen = ({ args, flags }) => {
|
|
97
|
+
const { exit } = useApp();
|
|
98
|
+
const featureName = args[0] ?? 'unnamed';
|
|
99
|
+
const description = args[1] ?? 'No description provided';
|
|
100
|
+
const mode = flags.brownfield ? 'brownfield' : 'greenfield';
|
|
101
|
+
const verbose = flags.verbose ?? false;
|
|
102
|
+
const repoPath = process.cwd();
|
|
103
|
+
// Initialize services
|
|
104
|
+
const servicesRef = useRef(null);
|
|
105
|
+
if (!servicesRef.current) {
|
|
106
|
+
const stateStore = createStateStore(repoPath);
|
|
107
|
+
const agentInvoker = createAgentInvoker();
|
|
108
|
+
const flowMachine = createExtendedFlowMachine();
|
|
109
|
+
const worktreeService = createWorktreeService();
|
|
110
|
+
const commitService = createCommitService();
|
|
111
|
+
const taskParser = createTaskParser();
|
|
112
|
+
const specInitService = createSpecInitService();
|
|
113
|
+
const healthCheck = createClaudeHealthCheck();
|
|
114
|
+
const gitStatusChecker = createGitStatusChecker();
|
|
115
|
+
const configService = createConfigService();
|
|
116
|
+
servicesRef.current = {
|
|
117
|
+
stateStore,
|
|
118
|
+
agentInvoker,
|
|
119
|
+
flowMachine,
|
|
120
|
+
worktreeService,
|
|
121
|
+
commitService,
|
|
122
|
+
taskParser,
|
|
123
|
+
specInitService,
|
|
124
|
+
healthCheck,
|
|
125
|
+
gitStatusChecker,
|
|
126
|
+
configService
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const services = servicesRef.current;
|
|
130
|
+
// Log file path
|
|
131
|
+
const logFileRef = useRef(null);
|
|
132
|
+
// Initialize log file
|
|
133
|
+
const initLogFile = useCallback(async (workDir) => {
|
|
134
|
+
const logDir = join(workDir, '.red64', 'flows', sanitizeFeatureName(featureName));
|
|
135
|
+
await mkdir(logDir, { recursive: true });
|
|
136
|
+
const logPath = join(logDir, 'flow.log');
|
|
137
|
+
logFileRef.current = logPath;
|
|
138
|
+
// Write header
|
|
139
|
+
const header = `\n${'='.repeat(60)}\nFlow started: ${new Date().toISOString()}\nFeature: ${featureName}\nMode: ${mode}\n${'='.repeat(60)}\n\n`;
|
|
140
|
+
await appendFile(logPath, header);
|
|
141
|
+
return logPath;
|
|
142
|
+
}, [featureName, mode]);
|
|
143
|
+
// Log to file
|
|
144
|
+
const logToFile = useCallback(async (message) => {
|
|
145
|
+
if (logFileRef.current) {
|
|
146
|
+
const timestamp = new Date().toISOString().split('T')[1].split('.')[0];
|
|
147
|
+
await appendFile(logFileRef.current, `[${timestamp}] ${message}\n`).catch(() => { });
|
|
148
|
+
}
|
|
149
|
+
}, []);
|
|
150
|
+
// Flow state
|
|
151
|
+
const [flowState, setFlowState] = useState({
|
|
152
|
+
phase: { type: 'idle' },
|
|
153
|
+
output: [],
|
|
154
|
+
error: null,
|
|
155
|
+
claudeError: null,
|
|
156
|
+
isExecuting: false,
|
|
157
|
+
isHealthChecking: false,
|
|
158
|
+
preStartStep: { type: 'checking' }, // Start by checking for existing flow
|
|
159
|
+
worktreePath: null,
|
|
160
|
+
currentTask: 0,
|
|
161
|
+
totalTasks: 0,
|
|
162
|
+
tasks: [],
|
|
163
|
+
resolvedFeatureName: null,
|
|
164
|
+
existingFlowState: null,
|
|
165
|
+
completedTasks: [], // Orchestrator-tracked completed task IDs
|
|
166
|
+
phaseMetrics: {}, // Phase timing metrics
|
|
167
|
+
commitCount: 0, // Number of commits for this feature
|
|
168
|
+
agent: 'claude' // Default, will be loaded from config
|
|
169
|
+
});
|
|
170
|
+
// Track if flow has been started
|
|
171
|
+
const flowStartedRef = useRef(false);
|
|
172
|
+
// Add output line (to screen and log file)
|
|
173
|
+
const addOutput = useCallback((line) => {
|
|
174
|
+
setFlowState(prev => ({
|
|
175
|
+
...prev,
|
|
176
|
+
output: [...prev.output.slice(-50), line]
|
|
177
|
+
}));
|
|
178
|
+
// Also log to file
|
|
179
|
+
logToFile(line);
|
|
180
|
+
}, [logToFile]);
|
|
181
|
+
// Get working directory (worktree or repo)
|
|
182
|
+
const getWorkingDir = useCallback(() => {
|
|
183
|
+
return flowState.worktreePath ?? repoPath;
|
|
184
|
+
}, [flowState.worktreePath, repoPath]);
|
|
185
|
+
// Execute a Claude command
|
|
186
|
+
const executeCommand = useCallback(async (prompt, workDir) => {
|
|
187
|
+
const dir = workDir ?? getWorkingDir();
|
|
188
|
+
// Build tier config dir path
|
|
189
|
+
const tierConfigDir = flags.tier
|
|
190
|
+
? `${process.env.HOME ?? '~'}/.claude-${flags.tier}`
|
|
191
|
+
: null;
|
|
192
|
+
// Always log command to file
|
|
193
|
+
await logToFile(`--- Executing command ---`);
|
|
194
|
+
await logToFile(`Command: claude -p "${prompt}"`);
|
|
195
|
+
await logToFile(`Working dir: ${dir}`);
|
|
196
|
+
if (flags.skipPermissions) {
|
|
197
|
+
await logToFile(`Flags: --dangerously-skip-permissions`);
|
|
198
|
+
}
|
|
199
|
+
if (tierConfigDir) {
|
|
200
|
+
await logToFile(`CLAUDE_CONFIG_DIR: ${tierConfigDir}`);
|
|
201
|
+
}
|
|
202
|
+
if (flags.sandbox) {
|
|
203
|
+
await logToFile(`Sandbox: Docker isolated mode`);
|
|
204
|
+
}
|
|
205
|
+
if (flags.model) {
|
|
206
|
+
await logToFile(`Model: ${flags.model}`);
|
|
207
|
+
}
|
|
208
|
+
// Verbose mode: also show on screen
|
|
209
|
+
if (verbose) {
|
|
210
|
+
addOutput(`[verbose] Command: claude -p "${prompt}"`);
|
|
211
|
+
addOutput(`[verbose] Working dir: ${dir}`);
|
|
212
|
+
if (flags.skipPermissions) {
|
|
213
|
+
addOutput(`[verbose] Flags: --dangerously-skip-permissions`);
|
|
214
|
+
}
|
|
215
|
+
if (tierConfigDir) {
|
|
216
|
+
addOutput(`[verbose] CLAUDE_CONFIG_DIR: ${tierConfigDir}`);
|
|
217
|
+
}
|
|
218
|
+
if (flags.sandbox) {
|
|
219
|
+
addOutput(`[verbose] Sandbox: Docker isolated mode`);
|
|
220
|
+
}
|
|
221
|
+
if (flags.model) {
|
|
222
|
+
addOutput(`[verbose] Model: ${flags.model}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
setFlowState(prev => ({ ...prev, isExecuting: true, error: null, claudeError: null }));
|
|
226
|
+
const result = await services.agentInvoker.invoke({
|
|
227
|
+
prompt,
|
|
228
|
+
workingDirectory: dir,
|
|
229
|
+
skipPermissions: flags.skipPermissions ?? false,
|
|
230
|
+
tier: flags.tier,
|
|
231
|
+
model: flags.model,
|
|
232
|
+
sandbox: flags.sandbox ?? false,
|
|
233
|
+
onOutput: (chunk) => {
|
|
234
|
+
// Stream output in real-time
|
|
235
|
+
const lines = chunk.split('\n').filter(l => l.trim());
|
|
236
|
+
lines.forEach(line => addOutput(line));
|
|
237
|
+
},
|
|
238
|
+
onError: (chunk) => {
|
|
239
|
+
// Stream stderr in verbose mode
|
|
240
|
+
if (verbose) {
|
|
241
|
+
const lines = chunk.split('\n').filter(l => l.trim());
|
|
242
|
+
lines.forEach(line => addOutput(`[stderr] ${line}`));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
setFlowState(prev => ({ ...prev, isExecuting: false }));
|
|
247
|
+
// Always log result to file
|
|
248
|
+
await logToFile(`Exit code: ${result.exitCode}`);
|
|
249
|
+
await logToFile(`Success: ${result.success}`);
|
|
250
|
+
if (result.timedOut) {
|
|
251
|
+
await logToFile(`Timed out: true`);
|
|
252
|
+
}
|
|
253
|
+
if (result.claudeError) {
|
|
254
|
+
await logToFile(`Claude Error: ${result.claudeError.code} - ${result.claudeError.message}`);
|
|
255
|
+
await logToFile(`Suggestion: ${result.claudeError.suggestion}`);
|
|
256
|
+
}
|
|
257
|
+
if (result.stdout) {
|
|
258
|
+
await logToFile(`--- stdout ---`);
|
|
259
|
+
await logToFile(result.stdout);
|
|
260
|
+
}
|
|
261
|
+
if (result.stderr) {
|
|
262
|
+
await logToFile(`--- stderr ---`);
|
|
263
|
+
await logToFile(result.stderr);
|
|
264
|
+
}
|
|
265
|
+
await logToFile(`--- end command ---\n`);
|
|
266
|
+
// Verbose mode: show result on screen
|
|
267
|
+
if (verbose) {
|
|
268
|
+
addOutput(`[verbose] Exit code: ${result.exitCode}`);
|
|
269
|
+
addOutput(`[verbose] Success: ${result.success}`);
|
|
270
|
+
if (result.timedOut) {
|
|
271
|
+
addOutput(`[verbose] Timed out: true`);
|
|
272
|
+
}
|
|
273
|
+
if (result.claudeError) {
|
|
274
|
+
addOutput(`[verbose] Claude Error: ${result.claudeError.code}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!result.success) {
|
|
278
|
+
// Use Claude error if detected, otherwise build generic error message
|
|
279
|
+
if (result.claudeError) {
|
|
280
|
+
const errorMsg = `${getClaudeErrorLabel(result.claudeError.code)}: ${result.claudeError.suggestion}`;
|
|
281
|
+
setFlowState(prev => ({ ...prev, error: errorMsg, claudeError: result.claudeError ?? null }));
|
|
282
|
+
return { success: false, output: result.stdout, error: errorMsg, claudeError: result.claudeError };
|
|
283
|
+
}
|
|
284
|
+
// Build generic error message
|
|
285
|
+
let errorMsg = 'Command failed';
|
|
286
|
+
if (result.timedOut) {
|
|
287
|
+
errorMsg = 'Command timed out (10 min limit)';
|
|
288
|
+
}
|
|
289
|
+
else if (result.stderr) {
|
|
290
|
+
errorMsg = result.stderr.trim().split('\n')[0]; // First line of stderr
|
|
291
|
+
}
|
|
292
|
+
else if (result.exitCode !== 0) {
|
|
293
|
+
errorMsg = `Command exited with code ${result.exitCode}`;
|
|
294
|
+
}
|
|
295
|
+
// In verbose mode, show full stderr on screen
|
|
296
|
+
if (verbose && result.stderr) {
|
|
297
|
+
addOutput(`[verbose] Full stderr:`);
|
|
298
|
+
result.stderr.split('\n').forEach(line => {
|
|
299
|
+
if (line.trim())
|
|
300
|
+
addOutput(` ${line}`);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
setFlowState(prev => ({ ...prev, error: errorMsg }));
|
|
304
|
+
return { success: false, output: result.stdout, error: errorMsg };
|
|
305
|
+
}
|
|
306
|
+
return { success: true, output: result.stdout };
|
|
307
|
+
}, [services.agentInvoker, flags, verbose, getWorkingDir, addOutput, logToFile]);
|
|
308
|
+
// Commit changes with formatted message
|
|
309
|
+
const commitChanges = useCallback(async (message, workDir) => {
|
|
310
|
+
const dir = workDir ?? getWorkingDir();
|
|
311
|
+
addOutput(`Committing: ${message.split('\n')[0]}...`);
|
|
312
|
+
const result = await services.commitService.stageAndCommit(dir, message);
|
|
313
|
+
if (!result.success) {
|
|
314
|
+
addOutput(`Commit warning: ${result.error ?? 'No changes to commit'}`);
|
|
315
|
+
return true; // Continue even if nothing to commit
|
|
316
|
+
}
|
|
317
|
+
if (result.commitHash) {
|
|
318
|
+
addOutput(`Committed: ${result.commitHash.substring(0, 7)}`);
|
|
319
|
+
// Increment commit count
|
|
320
|
+
setFlowState(prev => ({ ...prev, commitCount: prev.commitCount + 1 }));
|
|
321
|
+
}
|
|
322
|
+
return true;
|
|
323
|
+
}, [services.commitService, getWorkingDir, addOutput]);
|
|
324
|
+
// Save flow state with task progress and phase metrics
|
|
325
|
+
const saveFlowState = useCallback(async (phase, workDir, completedTasksOverride) => {
|
|
326
|
+
const dir = workDir ?? getWorkingDir();
|
|
327
|
+
const stateStore = createStateStore(dir);
|
|
328
|
+
// Load existing state to preserve createdAt and merge data
|
|
329
|
+
const existingState = await stateStore.load(featureName);
|
|
330
|
+
// Use override if provided, otherwise use current state, otherwise preserve existing
|
|
331
|
+
const completedTasks = completedTasksOverride ?? flowState.completedTasks;
|
|
332
|
+
const state = {
|
|
333
|
+
feature: featureName,
|
|
334
|
+
phase: convertToFlowPhase(phase),
|
|
335
|
+
createdAt: existingState?.createdAt ?? new Date().toISOString(),
|
|
336
|
+
updatedAt: new Date().toISOString(),
|
|
337
|
+
history: existingState?.history ?? [],
|
|
338
|
+
metadata: {
|
|
339
|
+
description,
|
|
340
|
+
mode,
|
|
341
|
+
tier: flags.tier,
|
|
342
|
+
worktreePath: flowState.worktreePath ?? undefined,
|
|
343
|
+
resolvedFeatureName: flowState.resolvedFeatureName ?? undefined
|
|
344
|
+
},
|
|
345
|
+
// Orchestrator-controlled task progress
|
|
346
|
+
taskProgress: completedTasks.length > 0 || flowState.totalTasks > 0 ? {
|
|
347
|
+
completedTasks,
|
|
348
|
+
totalTasks: flowState.totalTasks
|
|
349
|
+
} : existingState?.taskProgress,
|
|
350
|
+
// Phase timing metrics
|
|
351
|
+
phaseMetrics: Object.keys(flowState.phaseMetrics).length > 0
|
|
352
|
+
? { ...existingState?.phaseMetrics, ...flowState.phaseMetrics }
|
|
353
|
+
: existingState?.phaseMetrics
|
|
354
|
+
};
|
|
355
|
+
await stateStore.save(state);
|
|
356
|
+
}, [featureName, description, mode, flags.tier, flowState.worktreePath, flowState.resolvedFeatureName, flowState.completedTasks, flowState.totalTasks, flowState.phaseMetrics, getWorkingDir]);
|
|
357
|
+
// Transition to next phase
|
|
358
|
+
const transitionPhase = useCallback((event) => {
|
|
359
|
+
const nextPhase = services.flowMachine.send(event);
|
|
360
|
+
setFlowState(prev => ({ ...prev, phase: nextPhase }));
|
|
361
|
+
return nextPhase;
|
|
362
|
+
}, [services.flowMachine]);
|
|
363
|
+
// Check for existing flow on mount
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
if (flowStartedRef.current)
|
|
366
|
+
return;
|
|
367
|
+
flowStartedRef.current = true;
|
|
368
|
+
const checkExistingFlow = async () => {
|
|
369
|
+
// Load config to get the agent setting
|
|
370
|
+
const config = await services.configService.load(repoPath);
|
|
371
|
+
if (config?.agent) {
|
|
372
|
+
setFlowState(prev => ({ ...prev, agent: config.agent }));
|
|
373
|
+
}
|
|
374
|
+
addOutput('Checking for existing flow...');
|
|
375
|
+
// Check if there's an existing flow state for this feature
|
|
376
|
+
// First check main repo, then check worktree if exists
|
|
377
|
+
let existingState = await services.stateStore.load(featureName);
|
|
378
|
+
// If not found in main repo, check if worktree exists and has state
|
|
379
|
+
if (!existingState) {
|
|
380
|
+
const worktreeCheck = await services.worktreeService.check(repoPath, featureName);
|
|
381
|
+
if (worktreeCheck.exists && worktreeCheck.path) {
|
|
382
|
+
const worktreeStateStore = createStateStore(worktreeCheck.path);
|
|
383
|
+
existingState = await worktreeStateStore.load(featureName);
|
|
384
|
+
if (existingState) {
|
|
385
|
+
addOutput(`Found state in worktree: ${worktreeCheck.path}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (existingState && existingState.phase.type !== 'complete' && existingState.phase.type !== 'aborted') {
|
|
390
|
+
// Found an in-progress flow - check for uncommitted changes
|
|
391
|
+
const worktreePath = existingState.metadata.worktreePath ?? repoPath;
|
|
392
|
+
const gitStatus = await services.gitStatusChecker.check(worktreePath);
|
|
393
|
+
// Load task progress from state (source of truth)
|
|
394
|
+
const completedTasks = existingState.taskProgress?.completedTasks ?? [];
|
|
395
|
+
const totalTasks = existingState.taskProgress?.totalTasks ?? 0;
|
|
396
|
+
if (gitStatus.hasChanges) {
|
|
397
|
+
// Has uncommitted changes - prompt user first
|
|
398
|
+
setFlowState(prev => ({
|
|
399
|
+
...prev,
|
|
400
|
+
preStartStep: { type: 'uncommitted-changes', existingState, gitStatus },
|
|
401
|
+
existingFlowState: existingState,
|
|
402
|
+
worktreePath: existingState.metadata.worktreePath ?? null,
|
|
403
|
+
resolvedFeatureName: existingState.metadata.resolvedFeatureName ?? null,
|
|
404
|
+
completedTasks: [...completedTasks],
|
|
405
|
+
totalTasks
|
|
406
|
+
}));
|
|
407
|
+
addOutput(`Found existing flow at phase: ${existingState.phase.type}`);
|
|
408
|
+
if (completedTasks.length > 0) {
|
|
409
|
+
addOutput(`Completed tasks: ${completedTasks.join(', ')}`);
|
|
410
|
+
}
|
|
411
|
+
addOutput(`Uncommitted changes detected: ${gitStatus.staged} staged, ${gitStatus.unstaged} unstaged, ${gitStatus.untracked} untracked`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
// No uncommitted changes - prompt resume vs restart
|
|
415
|
+
setFlowState(prev => ({
|
|
416
|
+
...prev,
|
|
417
|
+
preStartStep: { type: 'existing-flow-detected', existingState, gitStatus },
|
|
418
|
+
existingFlowState: existingState,
|
|
419
|
+
worktreePath: existingState.metadata.worktreePath ?? null,
|
|
420
|
+
resolvedFeatureName: existingState.metadata.resolvedFeatureName ?? null,
|
|
421
|
+
completedTasks: [...completedTasks],
|
|
422
|
+
totalTasks
|
|
423
|
+
}));
|
|
424
|
+
addOutput(`Found existing flow at phase: ${existingState.phase.type}`);
|
|
425
|
+
if (completedTasks.length > 0) {
|
|
426
|
+
addOutput(`Completed tasks: ${completedTasks.join(', ')}`);
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
// No existing flow or flow is complete/aborted - proceed with fresh start
|
|
431
|
+
setFlowState(prev => ({ ...prev, preStartStep: { type: 'ready' } }));
|
|
432
|
+
await startFreshFlow();
|
|
433
|
+
};
|
|
434
|
+
checkExistingFlow();
|
|
435
|
+
}, []);
|
|
436
|
+
// Handle existing flow decision (resume vs restart)
|
|
437
|
+
const handleExistingFlowDecision = useCallback(async (decision) => {
|
|
438
|
+
if (decision === 'resume') {
|
|
439
|
+
await resumeExistingFlow();
|
|
440
|
+
}
|
|
441
|
+
else if (decision === 'restart') {
|
|
442
|
+
setFlowState(prev => ({ ...prev, preStartStep: { type: 'ready' }, existingFlowState: null }));
|
|
443
|
+
addOutput('Starting fresh flow...');
|
|
444
|
+
await startFreshFlow();
|
|
445
|
+
}
|
|
446
|
+
else if (decision === 'abort') {
|
|
447
|
+
exit();
|
|
448
|
+
}
|
|
449
|
+
}, [exit]);
|
|
450
|
+
// Handle uncommitted changes decision
|
|
451
|
+
const handleUncommittedChangesDecision = useCallback(async (decision) => {
|
|
452
|
+
const existingState = flowState.existingFlowState;
|
|
453
|
+
if (!existingState)
|
|
454
|
+
return;
|
|
455
|
+
const worktreePath = existingState.metadata.worktreePath ?? repoPath;
|
|
456
|
+
if (decision === 'commit') {
|
|
457
|
+
// Commit changes with WIP message
|
|
458
|
+
addOutput('Committing changes...');
|
|
459
|
+
const commitResult = await services.commitService.stageAndCommit(worktreePath, `WIP: ${featureName} - auto-commit before resume`);
|
|
460
|
+
if (commitResult.success) {
|
|
461
|
+
addOutput(`Committed: ${commitResult.commitHash?.substring(0, 7) ?? 'done'}`);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
addOutput(`Commit warning: ${commitResult.error ?? 'No changes to commit'}`);
|
|
465
|
+
}
|
|
466
|
+
// Now show resume vs restart choice
|
|
467
|
+
const gitStatus = await services.gitStatusChecker.check(worktreePath);
|
|
468
|
+
setFlowState(prev => ({
|
|
469
|
+
...prev,
|
|
470
|
+
preStartStep: { type: 'existing-flow-detected', existingState, gitStatus }
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
else if (decision === 'discard') {
|
|
474
|
+
// Discard changes using git checkout
|
|
475
|
+
addOutput('Discarding changes...');
|
|
476
|
+
const { spawn } = await import('node:child_process');
|
|
477
|
+
await new Promise((resolve) => {
|
|
478
|
+
const proc = spawn('git', ['checkout', '--', '.'], { cwd: worktreePath });
|
|
479
|
+
proc.on('close', () => {
|
|
480
|
+
// Also clean untracked files
|
|
481
|
+
const cleanProc = spawn('git', ['clean', '-fd'], { cwd: worktreePath });
|
|
482
|
+
cleanProc.on('close', () => resolve());
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
addOutput('Changes discarded');
|
|
486
|
+
// Now show resume vs restart choice
|
|
487
|
+
const gitStatus = await services.gitStatusChecker.check(worktreePath);
|
|
488
|
+
setFlowState(prev => ({
|
|
489
|
+
...prev,
|
|
490
|
+
preStartStep: { type: 'existing-flow-detected', existingState, gitStatus }
|
|
491
|
+
}));
|
|
492
|
+
}
|
|
493
|
+
else if (decision === 'abort') {
|
|
494
|
+
exit();
|
|
495
|
+
}
|
|
496
|
+
}, [flowState.existingFlowState, services.commitService, services.gitStatusChecker, featureName, repoPath, exit, addOutput]);
|
|
497
|
+
// Resume from existing flow state
|
|
498
|
+
const resumeExistingFlow = useCallback(async () => {
|
|
499
|
+
const existingState = flowState.existingFlowState;
|
|
500
|
+
if (!existingState) {
|
|
501
|
+
addOutput('Error: No existing flow state to resume');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const phaseType = existingState.phase.type;
|
|
505
|
+
addOutput(`Resuming from phase: ${phaseType}`);
|
|
506
|
+
// Initialize log file
|
|
507
|
+
const workDir = existingState.metadata.worktreePath ?? repoPath;
|
|
508
|
+
// Load initial commit count
|
|
509
|
+
const initialCommitCount = await services.commitService.countFeatureCommits(workDir);
|
|
510
|
+
setFlowState(prev => ({
|
|
511
|
+
...prev,
|
|
512
|
+
preStartStep: { type: 'resuming', fromPhase: phaseType },
|
|
513
|
+
isHealthChecking: true,
|
|
514
|
+
commitCount: initialCommitCount
|
|
515
|
+
}));
|
|
516
|
+
await initLogFile(workDir);
|
|
517
|
+
// Run health check
|
|
518
|
+
addOutput('Checking Claude API status...');
|
|
519
|
+
const healthResult = await services.healthCheck.check({
|
|
520
|
+
tier: flags.tier,
|
|
521
|
+
sandbox: flags.sandbox,
|
|
522
|
+
timeoutMs: 30000
|
|
523
|
+
});
|
|
524
|
+
setFlowState(prev => ({ ...prev, isHealthChecking: false }));
|
|
525
|
+
if (!healthResult.healthy) {
|
|
526
|
+
const errorMsg = healthResult.error
|
|
527
|
+
? `${getClaudeErrorLabel(healthResult.error.code)}: ${healthResult.error.suggestion}`
|
|
528
|
+
: healthResult.message;
|
|
529
|
+
setFlowState(prev => ({
|
|
530
|
+
...prev,
|
|
531
|
+
error: errorMsg,
|
|
532
|
+
claudeError: healthResult.error ?? null,
|
|
533
|
+
phase: { type: 'error', feature: featureName, error: errorMsg }
|
|
534
|
+
}));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
addOutput(`API ready (${healthResult.durationMs}ms)`);
|
|
538
|
+
// Set up flow machine to the current phase
|
|
539
|
+
const effectiveName = existingState.metadata.resolvedFeatureName ?? sanitizeFeatureName(featureName);
|
|
540
|
+
// Update state with existing flow info
|
|
541
|
+
setFlowState(prev => ({
|
|
542
|
+
...prev,
|
|
543
|
+
worktreePath: existingState.metadata.worktreePath ?? null,
|
|
544
|
+
resolvedFeatureName: effectiveName
|
|
545
|
+
}));
|
|
546
|
+
// Resume based on current phase
|
|
547
|
+
await resumeFromPhase(existingState.phase.type, workDir, effectiveName);
|
|
548
|
+
}, [flowState.existingFlowState, services.healthCheck, flags, featureName, repoPath, initLogFile, addOutput]);
|
|
549
|
+
// Resume from a specific phase
|
|
550
|
+
const resumeFromPhase = async (phaseType, workDir, effectiveName) => {
|
|
551
|
+
addOutput(`Continuing from ${phaseType}...`);
|
|
552
|
+
switch (phaseType) {
|
|
553
|
+
case 'requirements-review':
|
|
554
|
+
case 'requirements-approval':
|
|
555
|
+
// Waiting for approval - show approval UI
|
|
556
|
+
transitionPhase({ type: 'START', feature: featureName, description, mode });
|
|
557
|
+
transitionPhase({ type: 'PHASE_COMPLETE' }); // to requirements-generating
|
|
558
|
+
transitionPhase({ type: 'PHASE_COMPLETE' }); // to requirements-approval
|
|
559
|
+
break;
|
|
560
|
+
case 'design-generating':
|
|
561
|
+
// Resume design generation
|
|
562
|
+
transitionPhase({ type: 'START', feature: featureName, description, mode });
|
|
563
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
564
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
565
|
+
transitionPhase({ type: 'APPROVE' });
|
|
566
|
+
await runDesignPhase(workDir);
|
|
567
|
+
break;
|
|
568
|
+
case 'design-review':
|
|
569
|
+
case 'design-approval':
|
|
570
|
+
// Waiting for design approval
|
|
571
|
+
transitionPhase({ type: 'START', feature: featureName, description, mode });
|
|
572
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
573
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
574
|
+
transitionPhase({ type: 'APPROVE' });
|
|
575
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
576
|
+
break;
|
|
577
|
+
case 'tasks-generating':
|
|
578
|
+
// Resume tasks generation
|
|
579
|
+
transitionPhase({ type: 'START', feature: featureName, description, mode });
|
|
580
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
581
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
582
|
+
transitionPhase({ type: 'APPROVE' });
|
|
583
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
584
|
+
transitionPhase({ type: 'APPROVE' });
|
|
585
|
+
await runTasksPhase(workDir);
|
|
586
|
+
break;
|
|
587
|
+
case 'tasks-review':
|
|
588
|
+
case 'tasks-approval':
|
|
589
|
+
// Waiting for tasks approval - load ALL tasks
|
|
590
|
+
transitionPhase({ type: 'START', feature: featureName, description, mode });
|
|
591
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
592
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
593
|
+
transitionPhase({ type: 'APPROVE' });
|
|
594
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
595
|
+
transitionPhase({ type: 'APPROVE' });
|
|
596
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
597
|
+
{
|
|
598
|
+
// Load ALL tasks (not just pending by file checkbox)
|
|
599
|
+
const specDir = join(workDir, '.red64', 'specs', effectiveName);
|
|
600
|
+
const tasks = await services.taskParser.parse(specDir);
|
|
601
|
+
setFlowState(prev => ({
|
|
602
|
+
...prev,
|
|
603
|
+
tasks,
|
|
604
|
+
totalTasks: tasks.length
|
|
605
|
+
}));
|
|
606
|
+
}
|
|
607
|
+
break;
|
|
608
|
+
case 'implementing':
|
|
609
|
+
// Resume implementation - use state.json.completedTasks as source of truth
|
|
610
|
+
transitionPhase({ type: 'START', feature: featureName, description, mode });
|
|
611
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
612
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
613
|
+
transitionPhase({ type: 'APPROVE' });
|
|
614
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
615
|
+
transitionPhase({ type: 'APPROVE' });
|
|
616
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
617
|
+
transitionPhase({ type: 'APPROVE' });
|
|
618
|
+
{
|
|
619
|
+
// Load ALL tasks from file
|
|
620
|
+
const implSpecDir = join(workDir, '.red64', 'specs', effectiveName);
|
|
621
|
+
const implTasks = await services.taskParser.parse(implSpecDir);
|
|
622
|
+
// Use state.json.completedTasks as source of truth (already loaded)
|
|
623
|
+
const completedTaskIds = flowState.completedTasks;
|
|
624
|
+
// Sync tasks.md checkboxes if out of sync with state.json
|
|
625
|
+
for (const taskId of completedTaskIds) {
|
|
626
|
+
const task = implTasks.find(t => t.id === taskId);
|
|
627
|
+
if (task && !task.completed) {
|
|
628
|
+
addOutput(`Syncing task ${taskId} checkbox in tasks.md`);
|
|
629
|
+
await services.taskParser.markTaskComplete(implSpecDir, taskId);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// Filter pending using state.json (source of truth)
|
|
633
|
+
const implPendingTasks = implTasks.filter(t => !completedTaskIds.includes(t.id));
|
|
634
|
+
setFlowState(prev => ({
|
|
635
|
+
...prev,
|
|
636
|
+
tasks: implTasks,
|
|
637
|
+
totalTasks: implTasks.length
|
|
638
|
+
}));
|
|
639
|
+
if (implPendingTasks.length > 0) {
|
|
640
|
+
addOutput(`Resuming implementation: ${completedTaskIds.length} completed, ${implPendingTasks.length} pending`);
|
|
641
|
+
await runImplementation(workDir);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
addOutput('All tasks already completed!');
|
|
645
|
+
await completeFlow(workDir);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
break;
|
|
649
|
+
default:
|
|
650
|
+
// For other phases, start fresh
|
|
651
|
+
addOutput(`Cannot resume from phase ${phaseType}, starting fresh...`);
|
|
652
|
+
await startFreshFlow();
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
// Start a fresh flow (original startFlow logic)
|
|
656
|
+
const startFreshFlow = async () => {
|
|
657
|
+
// Initialize log file first (in repo, will move to worktree if created)
|
|
658
|
+
const logPath = await initLogFile(repoPath);
|
|
659
|
+
addOutput(`Log file: ${logPath}`);
|
|
660
|
+
addOutput('');
|
|
661
|
+
// Run health check before starting
|
|
662
|
+
addOutput('Checking Claude API status...');
|
|
663
|
+
setFlowState(prev => ({ ...prev, isHealthChecking: true }));
|
|
664
|
+
const healthResult = await services.healthCheck.check({
|
|
665
|
+
tier: flags.tier,
|
|
666
|
+
sandbox: flags.sandbox,
|
|
667
|
+
timeoutMs: 30000
|
|
668
|
+
});
|
|
669
|
+
setFlowState(prev => ({ ...prev, isHealthChecking: false }));
|
|
670
|
+
if (!healthResult.healthy) {
|
|
671
|
+
await logToFile(`Health check failed: ${healthResult.message}`);
|
|
672
|
+
if (healthResult.error) {
|
|
673
|
+
await logToFile(`Error code: ${healthResult.error.code}`);
|
|
674
|
+
await logToFile(`Suggestion: ${healthResult.error.suggestion}`);
|
|
675
|
+
}
|
|
676
|
+
// Set error state with Claude error details
|
|
677
|
+
const errorMsg = healthResult.error
|
|
678
|
+
? `${getClaudeErrorLabel(healthResult.error.code)}: ${healthResult.error.suggestion}`
|
|
679
|
+
: healthResult.message;
|
|
680
|
+
setFlowState(prev => ({
|
|
681
|
+
...prev,
|
|
682
|
+
error: errorMsg,
|
|
683
|
+
claudeError: healthResult.error ?? null,
|
|
684
|
+
phase: { type: 'error', feature: featureName, error: errorMsg }
|
|
685
|
+
}));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
addOutput(`API ready (${healthResult.durationMs}ms)`);
|
|
689
|
+
addOutput('');
|
|
690
|
+
addOutput(`Starting flow: ${featureName}`);
|
|
691
|
+
addOutput(`Mode: ${mode}`);
|
|
692
|
+
addOutput('');
|
|
693
|
+
// Step 1: Create or reuse git worktree for isolation
|
|
694
|
+
addOutput('Setting up git worktree for feature isolation...');
|
|
695
|
+
// Check if worktree already exists
|
|
696
|
+
const existingWorktree = await services.worktreeService.check(repoPath, featureName);
|
|
697
|
+
let workDir = repoPath;
|
|
698
|
+
if (existingWorktree.exists) {
|
|
699
|
+
// Reuse existing worktree
|
|
700
|
+
workDir = existingWorktree.path;
|
|
701
|
+
setFlowState(prev => ({ ...prev, worktreePath: existingWorktree.path }));
|
|
702
|
+
addOutput(`Using existing worktree: ${existingWorktree.path}`);
|
|
703
|
+
addOutput(`Branch: ${existingWorktree.branch}`);
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
// Create new worktree
|
|
707
|
+
const worktreeResult = await services.worktreeService.create(repoPath, featureName);
|
|
708
|
+
if (!worktreeResult.success) {
|
|
709
|
+
addOutput(`Worktree error: ${worktreeResult.error}`);
|
|
710
|
+
addOutput('Continuing without worktree isolation...');
|
|
711
|
+
// Continue without worktree
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
workDir = worktreeResult.path;
|
|
715
|
+
setFlowState(prev => ({ ...prev, worktreePath: worktreeResult.path }));
|
|
716
|
+
addOutput(`Worktree created: ${worktreeResult.path}`);
|
|
717
|
+
addOutput(`Branch: feature/${sanitizeFeatureName(featureName)}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
// Step 2: Transition to initializing
|
|
721
|
+
const initPhase = transitionPhase({
|
|
722
|
+
type: 'START',
|
|
723
|
+
feature: featureName,
|
|
724
|
+
description,
|
|
725
|
+
mode
|
|
726
|
+
});
|
|
727
|
+
await saveFlowState(initPhase, workDir);
|
|
728
|
+
// Initialize spec directory directly (no agent call needed)
|
|
729
|
+
addOutput('');
|
|
730
|
+
addOutput('Initializing spec directory...');
|
|
731
|
+
addOutput(`Working directory: ${workDir}`);
|
|
732
|
+
const initResult = await services.specInitService.init(workDir, featureName, description);
|
|
733
|
+
if (!initResult.success) {
|
|
734
|
+
transitionPhase({ type: 'ERROR', error: initResult.error ?? 'Initialization failed' });
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
// IMPORTANT: Update the resolved feature name from spec-init result
|
|
738
|
+
// This ensures all subsequent commands use the correct feature name
|
|
739
|
+
setFlowState(prev => ({ ...prev, resolvedFeatureName: initResult.featureName }));
|
|
740
|
+
addOutput(`Spec directory: ${initResult.specDir}`);
|
|
741
|
+
addOutput(`Feature name: ${initResult.featureName}`);
|
|
742
|
+
// Commit init
|
|
743
|
+
await commitChanges(`initialize spec directory`, workDir);
|
|
744
|
+
// Step 3: Generate requirements - pass the resolved feature name
|
|
745
|
+
const reqPhase = transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
746
|
+
await saveFlowState(reqPhase, workDir);
|
|
747
|
+
await runRequirementsPhase(workDir, initResult.featureName);
|
|
748
|
+
};
|
|
749
|
+
// Run requirements phase
|
|
750
|
+
const runRequirementsPhase = async (workDir, resolvedName) => {
|
|
751
|
+
// Use resolved name if provided, otherwise get from state or sanitize original
|
|
752
|
+
const effectiveName = resolvedName ?? flowState.resolvedFeatureName ?? sanitizeFeatureName(featureName);
|
|
753
|
+
addOutput('');
|
|
754
|
+
addOutput('Generating requirements...');
|
|
755
|
+
const result = await executeCommand(`/red64:spec-requirements ${effectiveName} -y`, workDir);
|
|
756
|
+
if (!result.success) {
|
|
757
|
+
transitionPhase({ type: 'ERROR', error: result.error ?? 'Requirements generation failed' });
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
// Commit requirements
|
|
761
|
+
await commitChanges(`generate requirements`, workDir);
|
|
762
|
+
// Transition to approval
|
|
763
|
+
const approvalPhase = transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
764
|
+
await saveFlowState(approvalPhase, workDir);
|
|
765
|
+
};
|
|
766
|
+
// Run design phase
|
|
767
|
+
const runDesignPhase = async (workDir) => {
|
|
768
|
+
const effectiveName = flowState.resolvedFeatureName ?? sanitizeFeatureName(featureName);
|
|
769
|
+
addOutput('');
|
|
770
|
+
addOutput('Generating technical design...');
|
|
771
|
+
const result = await executeCommand(`/red64:spec-design ${effectiveName} -y`, workDir);
|
|
772
|
+
if (!result.success) {
|
|
773
|
+
transitionPhase({ type: 'ERROR', error: result.error ?? 'Design generation failed' });
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
// Commit design
|
|
777
|
+
await commitChanges(`generate technical design`, workDir);
|
|
778
|
+
// Transition to approval
|
|
779
|
+
const approvalPhase = transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
780
|
+
await saveFlowState(approvalPhase, workDir);
|
|
781
|
+
};
|
|
782
|
+
// Run tasks phase
|
|
783
|
+
const runTasksPhase = async (workDir) => {
|
|
784
|
+
const effectiveName = flowState.resolvedFeatureName ?? sanitizeFeatureName(featureName);
|
|
785
|
+
addOutput('');
|
|
786
|
+
addOutput('Generating implementation tasks...');
|
|
787
|
+
const result = await executeCommand(`/red64:spec-tasks ${effectiveName} -y`, workDir);
|
|
788
|
+
if (!result.success) {
|
|
789
|
+
transitionPhase({ type: 'ERROR', error: result.error ?? 'Tasks generation failed' });
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
// Commit tasks
|
|
793
|
+
await commitChanges(`generate implementation tasks`, workDir);
|
|
794
|
+
// Parse tasks for implementation phase - use effective name for spec directory
|
|
795
|
+
const specDir = join(workDir, '.red64', 'specs', effectiveName);
|
|
796
|
+
const tasks = await services.taskParser.parse(specDir);
|
|
797
|
+
const pendingTasks = services.taskParser.getPendingTasks(tasks);
|
|
798
|
+
setFlowState(prev => ({
|
|
799
|
+
...prev,
|
|
800
|
+
tasks: pendingTasks,
|
|
801
|
+
totalTasks: pendingTasks.length
|
|
802
|
+
}));
|
|
803
|
+
addOutput(`Found ${pendingTasks.length} tasks to implement`);
|
|
804
|
+
// Transition to approval
|
|
805
|
+
const approvalPhase = transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
806
|
+
await saveFlowState(approvalPhase, workDir);
|
|
807
|
+
};
|
|
808
|
+
// Run implementation - one task at a time with commits
|
|
809
|
+
// ORCHESTRATOR-CONTROLLED: Marks tasks complete in both tasks.md and state.json
|
|
810
|
+
const runImplementation = async (workDir) => {
|
|
811
|
+
const { tasks, completedTasks: alreadyCompleted } = flowState;
|
|
812
|
+
const effectiveName = flowState.resolvedFeatureName ?? sanitizeFeatureName(featureName);
|
|
813
|
+
const specDir = join(workDir, '.red64', 'specs', effectiveName);
|
|
814
|
+
// Filter out already completed tasks (from state.json, source of truth)
|
|
815
|
+
const pendingTasks = tasks.filter(t => !alreadyCompleted.includes(t.id));
|
|
816
|
+
if (pendingTasks.length === 0) {
|
|
817
|
+
addOutput('No tasks to implement');
|
|
818
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
819
|
+
await completeFlow(workDir);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
addOutput('');
|
|
823
|
+
addOutput('Starting implementation...');
|
|
824
|
+
addOutput(`Total tasks: ${tasks.length}, Pending: ${pendingTasks.length}`);
|
|
825
|
+
if (alreadyCompleted.length > 0) {
|
|
826
|
+
addOutput(`Already completed: ${alreadyCompleted.join(', ')}`);
|
|
827
|
+
}
|
|
828
|
+
addOutput('');
|
|
829
|
+
// Track completed tasks in this run
|
|
830
|
+
let currentCompleted = [...alreadyCompleted];
|
|
831
|
+
// Execute each pending task one at a time
|
|
832
|
+
for (let i = 0; i < pendingTasks.length; i++) {
|
|
833
|
+
const task = pendingTasks[i];
|
|
834
|
+
const overallIndex = tasks.findIndex(t => t.id === task.id);
|
|
835
|
+
const taskNum = overallIndex + 1;
|
|
836
|
+
setFlowState(prev => ({ ...prev, currentTask: taskNum }));
|
|
837
|
+
addOutput(`[${currentCompleted.length + 1}/${tasks.length}] Task ${task.id}: ${task.title}`);
|
|
838
|
+
// Run spec-impl for this specific task - use effective name
|
|
839
|
+
const result = await executeCommand(`/red64:spec-impl ${effectiveName} ${task.id} -y`, workDir);
|
|
840
|
+
if (!result.success) {
|
|
841
|
+
addOutput(`Task ${task.id} failed: ${result.error}`);
|
|
842
|
+
// Save progress so far before continuing
|
|
843
|
+
await saveFlowState(flowState.phase, workDir, currentCompleted);
|
|
844
|
+
// Continue to next task instead of failing entirely
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
// ORCHESTRATOR-CONTROLLED TASK COMPLETION:
|
|
848
|
+
// 1. Mark task complete in tasks.md
|
|
849
|
+
const markResult = await services.taskParser.markTaskComplete(specDir, task.id);
|
|
850
|
+
if (!markResult.success) {
|
|
851
|
+
addOutput(`Warning: Failed to mark task ${task.id} in tasks.md: ${markResult.error}`);
|
|
852
|
+
}
|
|
853
|
+
// 2. Update state with completed task
|
|
854
|
+
currentCompleted = [...currentCompleted, task.id];
|
|
855
|
+
setFlowState(prev => ({ ...prev, completedTasks: currentCompleted }));
|
|
856
|
+
// 3. Save state immediately (before commit, for crash recovery)
|
|
857
|
+
await saveFlowState(flowState.phase, workDir, currentCompleted);
|
|
858
|
+
// 4. Commit both tasks.md and state.json together
|
|
859
|
+
await commitChanges(services.commitService.formatTaskCommitMessage(effectiveName, taskNum, task.title), workDir);
|
|
860
|
+
addOutput(`Task ${task.id} completed`);
|
|
861
|
+
addOutput('');
|
|
862
|
+
}
|
|
863
|
+
// All tasks complete
|
|
864
|
+
addOutput('All tasks completed!');
|
|
865
|
+
await completeFlow(workDir);
|
|
866
|
+
};
|
|
867
|
+
// Complete the flow
|
|
868
|
+
const completeFlow = async (workDir) => {
|
|
869
|
+
const effectiveName = flowState.resolvedFeatureName ?? sanitizeFeatureName(featureName);
|
|
870
|
+
const completePhase = { type: 'complete', feature: effectiveName };
|
|
871
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
872
|
+
await saveFlowState(completePhase, workDir);
|
|
873
|
+
addOutput('');
|
|
874
|
+
addOutput('Flow completed successfully!');
|
|
875
|
+
addOutput(`Worktree: ${flowState.worktreePath ?? 'none'}`);
|
|
876
|
+
addOutput(`Branch: feature/${sanitizeFeatureName(featureName)}`);
|
|
877
|
+
};
|
|
878
|
+
// Handle approval decision
|
|
879
|
+
const handleApproval = useCallback(async (decision) => {
|
|
880
|
+
const workDir = getWorkingDir();
|
|
881
|
+
const effectiveName = flowState.resolvedFeatureName ?? sanitizeFeatureName(featureName);
|
|
882
|
+
if (decision === 'approve') {
|
|
883
|
+
const nextPhase = transitionPhase({ type: 'APPROVE' });
|
|
884
|
+
await saveFlowState(nextPhase, workDir);
|
|
885
|
+
// Route to appropriate next phase
|
|
886
|
+
switch (nextPhase.type) {
|
|
887
|
+
case 'design-generating':
|
|
888
|
+
await runDesignPhase(workDir);
|
|
889
|
+
break;
|
|
890
|
+
case 'tasks-generating':
|
|
891
|
+
await runTasksPhase(workDir);
|
|
892
|
+
break;
|
|
893
|
+
case 'implementing':
|
|
894
|
+
// Update spec.json to mark tasks as approved before implementation
|
|
895
|
+
addOutput('Marking tasks as approved...');
|
|
896
|
+
const approvalResult = await services.specInitService.updateTaskApproval(workDir, effectiveName);
|
|
897
|
+
if (!approvalResult.success) {
|
|
898
|
+
addOutput(`Warning: Failed to update spec.json: ${approvalResult.error}`);
|
|
899
|
+
}
|
|
900
|
+
await runImplementation(workDir);
|
|
901
|
+
break;
|
|
902
|
+
case 'gap-analysis':
|
|
903
|
+
// Brownfield: run gap analysis
|
|
904
|
+
addOutput('Running gap analysis...');
|
|
905
|
+
const gapResult = await executeCommand(`/red64:validate-gap ${effectiveName} -y`, workDir);
|
|
906
|
+
if (gapResult.success) {
|
|
907
|
+
await commitChanges(`gap analysis`, workDir);
|
|
908
|
+
}
|
|
909
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
910
|
+
break;
|
|
911
|
+
case 'design-validation':
|
|
912
|
+
// Brownfield: run design validation
|
|
913
|
+
addOutput('Validating design...');
|
|
914
|
+
const valResult = await executeCommand(`/red64:validate-design ${effectiveName} -y`, workDir);
|
|
915
|
+
if (valResult.success) {
|
|
916
|
+
await commitChanges(`design validation`, workDir);
|
|
917
|
+
}
|
|
918
|
+
transitionPhase({ type: 'PHASE_COMPLETE' });
|
|
919
|
+
break;
|
|
920
|
+
case 'complete':
|
|
921
|
+
await completeFlow(workDir);
|
|
922
|
+
break;
|
|
923
|
+
default:
|
|
924
|
+
addOutput(`Unexpected phase: ${nextPhase.type}`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
else if (decision === 'reject') {
|
|
928
|
+
const prevPhase = transitionPhase({ type: 'REJECT' });
|
|
929
|
+
await saveFlowState(prevPhase, workDir);
|
|
930
|
+
addOutput('Regenerating...');
|
|
931
|
+
// Re-run the appropriate generation phase
|
|
932
|
+
switch (prevPhase.type) {
|
|
933
|
+
case 'requirements-generating':
|
|
934
|
+
await runRequirementsPhase(workDir);
|
|
935
|
+
break;
|
|
936
|
+
case 'design-generating':
|
|
937
|
+
await runDesignPhase(workDir);
|
|
938
|
+
break;
|
|
939
|
+
case 'tasks-generating':
|
|
940
|
+
await runTasksPhase(workDir);
|
|
941
|
+
break;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
else if (decision === 'pause') {
|
|
945
|
+
addOutput('Flow paused. Run the same start command to resume.');
|
|
946
|
+
addOutput(`Worktree: ${flowState.worktreePath ?? repoPath}`);
|
|
947
|
+
exit();
|
|
948
|
+
}
|
|
949
|
+
}, [flowState, transitionPhase, saveFlowState, getWorkingDir, exit]);
|
|
950
|
+
// Check if current phase is an approval phase
|
|
951
|
+
const isApprovalPhase = [
|
|
952
|
+
'requirements-approval',
|
|
953
|
+
'design-approval',
|
|
954
|
+
'tasks-approval',
|
|
955
|
+
'gap-review',
|
|
956
|
+
'design-validation-review',
|
|
957
|
+
'merge-decision'
|
|
958
|
+
].includes(flowState.phase.type);
|
|
959
|
+
// Render phase indicator
|
|
960
|
+
const renderPhaseIndicator = () => {
|
|
961
|
+
const phaseInfo = PHASE_LABELS[flowState.phase.type] ?? { label: flowState.phase.type, description: '' };
|
|
962
|
+
return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: phaseInfo.label }), _jsxs(Text, { dimColor: true, children: [" - ", phaseInfo.description] }), flowState.currentTask > 0 && flowState.totalTasks > 0 && (_jsxs(Text, { dimColor: true, children: [" [", flowState.currentTask, "/", flowState.totalTasks, "]"] }))] }));
|
|
963
|
+
};
|
|
964
|
+
// Render terminal phases
|
|
965
|
+
if (flowState.phase.type === 'complete') {
|
|
966
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: "green", children: "Flow Complete" }) }), _jsxs(Text, { children: ["Feature \"", featureName, "\" has been implemented."] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Spec: .red64/specs/", sanitizeFeatureName(featureName), "/"] }), _jsxs(Text, { dimColor: true, children: ["Branch: feature/", sanitizeFeatureName(featureName)] }), flowState.worktreePath && (_jsxs(Text, { dimColor: true, children: ["Worktree: ", flowState.worktreePath] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Next: Review changes and create a PR" }) })] })] }));
|
|
967
|
+
}
|
|
968
|
+
if (flowState.phase.type === 'error') {
|
|
969
|
+
const logPath = logFileRef.current ?? join(repoPath, '.red64', 'flows', sanitizeFeatureName(featureName), 'flow.log');
|
|
970
|
+
const claudeError = flowState.claudeError;
|
|
971
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: "red", children: claudeError ? `API Error: ${getClaudeErrorLabel(claudeError.code)}` : 'Flow Error' }) }), claudeError ? (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: "red", children: claudeError.message }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "Suggestion: " }), _jsx(Text, { color: "yellow", children: claudeError.suggestion })] }), !claudeError.recoverable && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", dimColor: true, children: "This error requires manual intervention before retrying." }) }))] })) : (_jsx(Text, { color: "red", children: flowState.error ?? 'An unknown error occurred' })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Feature: ", featureName] }), flowState.worktreePath && (_jsxs(Text, { dimColor: true, children: ["Worktree: ", flowState.worktreePath] })), !claudeError && (_jsxs(Text, { dimColor: true, children: ["Phase: ", flowState.phase.type] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Log file:" }), _jsx(Text, { color: "yellow", children: logPath })] }), claudeError?.recoverable && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Run \"red64 start ", sanitizeFeatureName(featureName), "\" to retry."] }) }))] })] }));
|
|
972
|
+
}
|
|
973
|
+
if (flowState.phase.type === 'aborted') {
|
|
974
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: "yellow", children: "Flow Aborted" }) }), _jsxs(Text, { children: ["Feature flow \"", featureName, "\" was aborted."] })] }));
|
|
975
|
+
}
|
|
976
|
+
// Should sidebar be shown?
|
|
977
|
+
const showSidebar = flowState.worktreePath !== null || flowState.phase.type !== 'idle';
|
|
978
|
+
return (_jsxs(Box, { flexDirection: "row", paddingX: 1, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "red64 start" }), _jsxs(Text, { dimColor: true, children: [" - ", featureName] })] }), flowState.worktreePath && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: ["Worktree: ", flowState.worktreePath] }) })), renderPhaseIndicator(), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: flowState.output.slice(-10).map((line, i) => (_jsx(Text, { dimColor: i < flowState.output.length - 1, children: line }, i))) }), flowState.isHealthChecking && (_jsx(Box, { marginBottom: 1, children: _jsx(Spinner, { label: "Checking Claude API status..." }) })), flowState.isExecuting && !flowState.isHealthChecking && (_jsx(Box, { marginBottom: 1, children: _jsx(Spinner, { label: "Processing..." }) })), flowState.error && !flowState.isExecuting && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "red", children: flowState.error }) })), flowState.preStartStep.type === 'existing-flow-detected' && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "yellow", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "Existing Flow Detected" }), _jsxs(Text, { dimColor: true, children: ["Phase: ", flowState.preStartStep.existingState.phase.type] }), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: EXISTING_FLOW_OPTIONS, onChange: handleExistingFlowDecision }) })] })), flowState.preStartStep.type === 'uncommitted-changes' && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "yellow", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: "Uncommitted Changes Detected" }), _jsxs(Text, { dimColor: true, children: [flowState.preStartStep.gitStatus.staged, " staged, ", flowState.preStartStep.gitStatus.unstaged, " unstaged, ", flowState.preStartStep.gitStatus.untracked, " untracked"] }), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: UNCOMMITTED_CHANGES_OPTIONS, onChange: handleUncommittedChangesDecision }) })] })), isApprovalPhase && !flowState.isExecuting && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Review Required" }), _jsxs(Text, { dimColor: true, children: ["Review output in .red64/specs/", sanitizeFeatureName(featureName), "/"] }), _jsx(Box, { marginTop: 1, children: _jsx(Select, { options: APPROVAL_OPTIONS, onChange: handleApproval }) })] }))] }), showSidebar && (_jsx(FeatureSidebar, { featureName: flowState.resolvedFeatureName ?? sanitizeFeatureName(featureName), sandboxMode: flags.sandbox ?? false, currentPhase: flowState.phase.type, mode: mode, currentTask: flowState.currentTask, totalTasks: flowState.totalTasks, commitCount: flowState.commitCount, agent: flowState.agent, model: flags.model }))] }));
|
|
979
|
+
};
|
|
980
|
+
/**
|
|
981
|
+
* Convert ExtendedFlowPhase to FlowPhase for state persistence
|
|
982
|
+
*/
|
|
983
|
+
function convertToFlowPhase(phase) {
|
|
984
|
+
switch (phase.type) {
|
|
985
|
+
case 'idle':
|
|
986
|
+
return { type: 'idle' };
|
|
987
|
+
case 'initializing':
|
|
988
|
+
return { type: 'initializing', feature: phase.feature, description: phase.description };
|
|
989
|
+
case 'requirements-generating':
|
|
990
|
+
return { type: 'requirements-generating', feature: phase.feature };
|
|
991
|
+
case 'requirements-approval':
|
|
992
|
+
return { type: 'requirements-review', feature: phase.feature };
|
|
993
|
+
case 'design-generating':
|
|
994
|
+
return { type: 'design-generating', feature: phase.feature };
|
|
995
|
+
case 'design-approval':
|
|
996
|
+
return { type: 'design-review', feature: phase.feature };
|
|
997
|
+
case 'tasks-generating':
|
|
998
|
+
return { type: 'tasks-generating', feature: phase.feature };
|
|
999
|
+
case 'tasks-approval':
|
|
1000
|
+
return { type: 'tasks-review', feature: phase.feature };
|
|
1001
|
+
case 'implementing':
|
|
1002
|
+
return {
|
|
1003
|
+
type: 'implementing',
|
|
1004
|
+
feature: phase.feature,
|
|
1005
|
+
currentTask: phase.currentTask,
|
|
1006
|
+
totalTasks: phase.totalTasks
|
|
1007
|
+
};
|
|
1008
|
+
case 'complete':
|
|
1009
|
+
return { type: 'complete', feature: phase.feature };
|
|
1010
|
+
case 'aborted':
|
|
1011
|
+
return { type: 'aborted', feature: phase.feature, reason: phase.reason };
|
|
1012
|
+
case 'error':
|
|
1013
|
+
return { type: 'error', feature: phase.feature, error: phase.error };
|
|
1014
|
+
default:
|
|
1015
|
+
if ('feature' in phase) {
|
|
1016
|
+
return { type: 'idle' };
|
|
1017
|
+
}
|
|
1018
|
+
return { type: 'idle' };
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
//# sourceMappingURL=StartScreen.js.map
|