diff --git a/cookbooks/scripts/tree-runner.sh b/cookbooks/scripts/tree-runner.sh index 3e9e5ba..5b1f015 100755 --- a/cookbooks/scripts/tree-runner.sh +++ b/cookbooks/scripts/tree-runner.sh @@ -847,16 +847,34 @@ cmd_teardown() { local action description action=$(echo "$step" | jq -r '.action // "unknown"') - description=$(echo "$step" | jq -r '.description // "Teardown step $i"') + description=$(echo "$step" | jq -r ".description // \"Teardown step $i\"") echo -e "${CYAN}Teardown $i:${NC} $description" + local response="" case "$action" in api) - execute_api_step "$step" > /dev/null && print_success "Done" || print_warning "Failed (continuing)" + if response=$(execute_api_step "$step" 2>/dev/null); then + # Check for error in response body + local api_error + api_error=$(echo "$response" | jq -r '.error // empty' 2>/dev/null) + if [[ -n "$api_error" ]]; then + print_warning "API error (continuing): $api_error" + else + local api_status + api_status=$(echo "$response" | jq -r '.data.status // "ok"' 2>/dev/null) + print_success "Done ($api_status)" + fi + else + print_warning "Failed (continuing)" + fi ;; shell) - execute_shell_step "$step" > /dev/null && print_success "Done" || print_warning "Failed (continuing)" + if response=$(execute_shell_step "$step" 2>/dev/null); then + print_success "Done" + else + print_warning "Failed (continuing)" + fi ;; *) print_warning "Skipping unknown action: $action" diff --git a/cookbooks/trees/foundary.yaml b/cookbooks/trees/foundary.yaml index f85c4ad..609c549 100644 --- a/cookbooks/trees/foundary.yaml +++ b/cookbooks/trees/foundary.yaml @@ -36,9 +36,31 @@ steps: max_attempts: 720 poll_interval: 5 + setup-hooks: + description: "Configure git hooks in project workspace" + depends_on: [wait-bootstrap] + action: api + method: POST + endpoint: "/projects/{{ .outputs.create-project.project_id }}/builds" + body: + prompt: "Run ./scripts/setup-hooks.sh to configure git hooks. Then verify with: git config core.hooksPath" + auto_commit: false + auto_push: false + git_clone_url: "{{ .outputs.create-project.git_clone_http }}" + outputs: + - build_id: .data.task_id + + wait-setup-hooks: + description: "Wait for git hooks setup to complete" + depends_on: [setup-hooks] + action: wait_build + build_id: "{{ .outputs.setup-hooks.build_id }}" + max_attempts: 120 + poll_interval: 5 + add-components: description: "Add React frontend, API service, and Postgres database" - depends_on: [wait-bootstrap] + depends_on: [wait-setup-hooks] action: api method: POST endpoint: "/projects/{{ .outputs.create-project.project_id }}/components/batch" @@ -255,13 +277,68 @@ steps: project_id: "{{ .outputs.create-project.project_id }}" max_attempts: 720 + # --- Commit QA artifacts and validate pre-merge hooks --- + commit-after-qa: + description: "Commit any remaining changes after QA" + depends_on: [wait-deploy-polish] + action: api + method: POST + endpoint: "/projects/{{ .outputs.create-project.project_id }}/builds" + body: + prompt: '/commit-all "chore: commit QA artifacts and fixes"' + auto_commit: false + auto_push: false + git_clone_url: "{{ .outputs.create-project.git_clone_http }}" + outputs: + - build_id: .data.task_id + + wait-commit-after-qa: + description: "Wait for QA commit to complete" + depends_on: [commit-after-qa] + action: wait_build + build_id: "{{ .outputs.commit-after-qa.build_id }}" + max_attempts: 120 + poll_interval: 5 + + pre-merge-validate: + description: "Run pre-commit hooks and fix any failures" + depends_on: [wait-commit-after-qa] + action: api + method: POST + endpoint: "/projects/{{ .outputs.create-project.project_id }}/builds" + body: + prompt: | + Run ./.githooks/pre-commit to check code quality. + If it fails, run /fix-all to fix all issues, then re-run + ./.githooks/pre-commit. Repeat until it passes. + auto_commit: true + auto_push: true + git_clone_url: "{{ .outputs.create-project.git_clone_http }}" + outputs: + - build_id: .data.task_id + + wait-pre-merge-validate: + description: "Wait for pre-merge validation to complete" + depends_on: [pre-merge-validate] + action: wait_build + build_id: "{{ .outputs.pre-merge-validate.build_id }}" + max_attempts: 720 + poll_interval: 5 + + wait-deploy-final: + description: "Wait for final deployment pipeline after validation fixes" + depends_on: [wait-pre-merge-validate] + action: wait_pipeline + project_id: "{{ .outputs.create-project.project_id }}" + max_attempts: 720 + # ============================================================ # SECTION 3: VERIFY # Confirm site is live and API responds # ============================================================ verify-site-live: description: "Verify site is live after all builds" - depends_on: [wait-deploy-polish] + depends_on: [wait-deploy-final] action: wait_site domain: "{{ .outputs.create-project.domain }}" max_attempts: 120 diff --git a/internal/adapter/templates/templates/components/app-react/src/App.tsx.tmpl b/internal/adapter/templates/templates/components/app-react/src/App.tsx.tmpl index 44b3f0c..3acfd3b 100644 --- a/internal/adapter/templates/templates/components/app-react/src/App.tsx.tmpl +++ b/internal/adapter/templates/templates/components/app-react/src/App.tsx.tmpl @@ -1,3 +1,4 @@ +import { Routes, Route, useLocation, useNavigate } from 'react-router-dom'; import { DashboardShell, Sidebar, Header, type NavItem } from '@{{PROJECT_NAME}}/layout'; import { Button, @@ -14,13 +15,239 @@ import { } from '@{{PROJECT_NAME}}/ui'; const navItems: NavItem[] = [ - { label: 'Dashboard', href: '/', icon: Home, active: true }, + { label: 'Dashboard', href: '/', icon: Home }, { label: 'Analytics', href: '/analytics', icon: BarChart3 }, { label: 'Users', href: '/users', icon: Users, badge: '12' }, { label: 'Settings', href: '/settings', icon: Settings }, ]; +const pageTitles: Record = { + '/': 'Dashboard', + '/analytics': 'Analytics', + '/users': 'Users', + '/settings': 'Settings', +}; + +function DashboardPage() { + return ( +
+ + + Welcome to {{COMPONENT_NAME}} + + This is part of the{' '} + + {{PROJECT_NAME}} + {' '} + monorepo, using the shared UI library and layout components. + + + +
+ + +
+
+
+ +
+ + + Total Users + 1,234 + + + +12% from last month + + + + + + Active Sessions + 567 + + + Live + + + + + + API Requests + 89.2k + + + High traffic + + +
+ +

+ Edit this file at{' '} + + apps/{{COMPONENT_NAME}}/src/App.tsx + +

+
+ ); +} + +function UsersPage() { + return ( +
+
+
+

All Users

+

Manage your team members and their roles.

+
+ +
+ +
+ {[ + { name: 'Alice Chen', role: 'Admin', status: 'Active' }, + { name: 'Bob Martinez', role: 'Editor', status: 'Active' }, + { name: 'Carol Singh', role: 'Viewer', status: 'Invited' }, + ].map((user) => ( + + + {user.name} + {user.role} + + + + {user.status} + + + + ))} +
+
+ ); +} + +function AnalyticsPage() { + return ( +
+
+ + + Page Views + 24.5k + + + +8% this week + + + + + + Bounce Rate + 32% + + + -3% improvement + + + + + + Avg. Session + 4m 12s + + + Stable + + +
+ + + + Traffic Sources + Where your visitors are coming from this month. + + +
+ {[ + { source: 'Direct', visits: '8,421', pct: 34 }, + { source: 'Search', visits: '6,312', pct: 26 }, + { source: 'Social', visits: '5,105', pct: 21 }, + { source: 'Referral', visits: '4,662', pct: 19 }, + ].map((row) => ( +
+ {row.source} +
+
+
+
+ {row.visits} +
+
+ ))} +
+ + +
+ ); +} + +function SettingsPage() { + return ( +
+ + + General + Manage your application settings and preferences. + + +
+ {[ + { label: 'Application Name', value: '{{PROJECT_NAME}}' }, + { label: 'Environment', value: 'Production' }, + { label: 'Region', value: 'US West' }, + ].map((setting) => ( +
+ {setting.label} + {setting.value} +
+ ))} +
+
+
+ + + + Danger Zone + Irreversible actions for your application. + + +
+
+

Delete Application

+

Permanently remove this application and all its data.

+
+ +
+
+
+
+ ); +} + function App() { + const location = useLocation(); + const navigate = useNavigate(); + + const itemsWithActive = navItems.map((item) => ({ + ...item, + active: location.pathname === item.href, + })); + + const pageTitle = pageTitles[location.pathname] || 'Dashboard'; + return ( {{PROJECT_NAME}} } - items={navItems} + items={itemsWithActive} + onNavigate={(href) => navigate(href)} footer={
v0.0.1 @@ -38,74 +266,18 @@ function App() { } header={
} > -
- {/* Welcome card */} - - - Welcome to {{COMPONENT_NAME}} - - This is part of the{' '} - - {{PROJECT_NAME}} - {' '} - monorepo, using the shared UI library and layout components. - - - -
- - -
-
-
- - {/* Stats cards */} -
- - - Total Users - 1,234 - - - +12% from last month - - - - - - Active Sessions - 567 - - - Live - - - - - - API Requests - 89.2k - - - High traffic - - -
- - {/* Edit hint */} -

- Edit this file at{' '} - - apps/{{COMPONENT_NAME}}/src/App.tsx - -

-
+ + } /> + } /> + } /> + } /> + ); } diff --git a/internal/adapter/templates/templates/components/app-react/src/main.tsx.tmpl b/internal/adapter/templates/templates/components/app-react/src/main.tsx.tmpl index 174dfe8..d1a78ed 100644 --- a/internal/adapter/templates/templates/components/app-react/src/main.tsx.tmpl +++ b/internal/adapter/templates/templates/components/app-react/src/main.tsx.tmpl @@ -1,11 +1,14 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; import App from './App.tsx'; import './index.css'; import './lib/logger'; ReactDOM.createRoot(document.getElementById('root')!).render( - + + + ); diff --git a/internal/handlers/architect.go b/internal/handlers/architect.go index fcc2c44..78961fb 100644 --- a/internal/handlers/architect.go +++ b/internal/handlers/architect.go @@ -7,6 +7,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/auth" "github.com/orchard9/rdev/internal/domain" + "github.com/orchard9/rdev/internal/logging" "github.com/orchard9/rdev/internal/service" "github.com/orchard9/rdev/internal/validate" "github.com/orchard9/rdev/pkg/api" @@ -62,6 +63,8 @@ func (h *ArchitectHandler) StartConversation(w http.ResponseWriter, r *http.Requ conv, err := h.architectService.StartConversation(r.Context(), projectID, req.Prompt) if err != nil { + log := logging.FromContext(r.Context()).WithHandler("StartConversation") + log.Error("failed to start conversation", logging.FieldError, err, logging.FieldProjectID, projectID) api.WriteInternalError(w, r, "failed to start conversation") return } @@ -100,6 +103,8 @@ func (h *ArchitectHandler) ContinueConversation(w http.ResponseWriter, r *http.R api.WriteNotFound(w, r, "conversation not found") return } + log := logging.FromContext(r.Context()).WithHandler("ContinueConversation") + log.Error("failed to continue conversation", logging.FieldError, err, "conversation_id", conversationID) api.WriteInternalError(w, r, "failed to continue conversation") return } @@ -138,6 +143,8 @@ func (h *ArchitectHandler) GenerateBlueprint(w http.ResponseWriter, r *http.Requ api.WriteNotFound(w, r, "conversation not found") return } + log := logging.FromContext(r.Context()).WithHandler("GenerateBlueprint") + log.Error("failed to generate blueprint", logging.FieldError, err, "conversation_id", conversationID) api.WriteInternalError(w, r, "failed to generate blueprint") return } diff --git a/internal/service/architect_service.go b/internal/service/architect_service.go index 5c183b1..6250989 100644 --- a/internal/service/architect_service.go +++ b/internal/service/architect_service.go @@ -150,9 +150,20 @@ func (s *ArchitectService) askArchitect(ctx context.Context, projectID string, c return "", fmt.Errorf("no agent available") } + // Resolve project pod name. The in-memory project repo may not have the + // project (it discovers pods in the rdev namespace, but projects deploy to + // the projects namespace). Fall back to defaults when not found. + podName := s.defaultPodName project, err := s.projectRepo.Get(ctx, domain.ProjectID(projectID)) if err != nil { - return "", fmt.Errorf("resolve project: %w", err) + log := logging.FromContext(ctx) + log.Warn("project not found in repo, using default pod", + logging.FieldProjectID, projectID, + "default_pod", podName, + logging.FieldError, err, + ) + } else if project.PodName != "" { + podName = project.PodName } // Prepare architect-specific system prompt @@ -175,15 +186,9 @@ Current conversation context:` fullPrompt := systemPrompt + "\n\n" + prompt - // Resolve pod: use project's pod if set, otherwise fall back to default. - podName := project.PodName - if podName == "" { - podName = s.defaultPodName - } - agentReq := &domain.AgentRequest{ Prompt: fullPrompt, - ProjectID: project.ID, + ProjectID: domain.ProjectID(projectID), Timeout: 2 * time.Minute, Metadata: map[string]string{ "conversation_id": string(conversationID), @@ -224,9 +229,17 @@ func (s *ArchitectService) extractSpecFromMessages(ctx context.Context, projectI return nil, fmt.Errorf("no agent available") } + // Resolve project pod name with fallback (same as askArchitect). + podName := s.defaultPodName project, err := s.projectRepo.Get(ctx, domain.ProjectID(projectID)) if err != nil { - return nil, fmt.Errorf("resolve project: %w", err) + log := logging.FromContext(ctx) + log.Warn("project not found in repo for spec extraction, using default pod", + logging.FieldProjectID, projectID, + logging.FieldError, err, + ) + } else if project.PodName != "" { + podName = project.PodName } // Build conversation transcript @@ -267,15 +280,9 @@ Extract and return ONLY a valid JSON object with this structure: Return ONLY the JSON, no other text.`, transcript) - // Resolve pod: use project's pod if set, otherwise fall back to default. - podName := project.PodName - if podName == "" { - podName = s.defaultPodName - } - agentReq := &domain.AgentRequest{ Prompt: extractionPrompt, - ProjectID: project.ID, + ProjectID: domain.ProjectID(projectID), Timeout: 2 * time.Minute, Metadata: map[string]string{ "purpose": "spec-extraction", diff --git a/internal/service/project_infra_crud.go b/internal/service/project_infra_crud.go index a78fdc5..85583d3 100644 --- a/internal/service/project_infra_crud.go +++ b/internal/service/project_infra_crud.go @@ -691,8 +691,9 @@ func (s *ProjectInfraService) DeleteProject(ctx context.Context, projectID strin return err } - // 1. Undeploy if deployed - if s.deployer != nil && status.DeploymentStatus != "none" { + // 1. Undeploy all K8s resources (always attempt — component deployments + // may exist even when the main deployment status is "none") + if s.deployer != nil { if err := s.deployer.UndeployAll(ctx, projectID); err != nil { log.Warn("failed to undeploy", logging.FieldError, err) }