fix(architect): add pod_name to agent requests, rewrite foundary cookbook
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

The architect service was missing pod_name/namespace in AgentRequest
metadata, causing Claude Code adapter to reject all requests. Added
ArchitectServiceConfig with pod resolution (project PodName → default
claudebox-0). Removed silent JSON fallback in extractSpecFromMessages
that masked errors.

Rewrote foundary cookbook from 90-step SDLC flow to focused 25-step
cookbook using natural language build prompts instead of /slash-commands
that claudebox cannot execute. Added "no fallbacks" rule to CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jordan 2026-02-11 01:24:34 -07:00
parent a9ad3d8304
commit c68fadbccd
4 changed files with 176 additions and 862 deletions

View File

@ -70,6 +70,7 @@ When discussing code: "add to **platform**" = edit rdev; "add to **skeleton**" =
- **Migrations:** NEVER modify committed migrations. Create NEW ones. - **Migrations:** NEVER modify committed migrations. Create NEW ones.
- **500-line limit:** Files exceeding 500 lines must be split - **500-line limit:** Files exceeding 500 lines must be split
- **Tests:** All handlers and services require tests - **Tests:** All handlers and services require tests
- **No fallbacks:** NEVER design "try X, fall back to Y" flows — fix X. Fallbacks hide errors and deliver inferior experiences.
- **Multi-step ops:** NEVER log-and-continue after partial failure. Rollback or document partial state. - **Multi-step ops:** NEVER log-and-continue after partial failure. Rollback or document partial state.
- **Logging:** Use `logging.FromContext(ctx)` or injected `*slog.Logger`. NEVER `fmt.Println`, `log.Fatal`, `log.Printf`, or bare `slog.Info()`. Error key is ALWAYS `"error"` (not `"err"`). Use field constants from `internal/logging/fields.go` (e.g., `logging.FieldProjectID`, `logging.FieldError`). Log once at boundary (handlers/workers log, services return errors). Sensitive data (passwords, tokens, keys) is auto-redacted. - **Logging:** Use `logging.FromContext(ctx)` or injected `*slog.Logger`. NEVER `fmt.Println`, `log.Fatal`, `log.Printf`, or bare `slog.Info()`. Error key is ALWAYS `"error"` (not `"err"`). Use field constants from `internal/logging/fields.go` (e.g., `logging.FieldProjectID`, `logging.FieldError`). Log once at boundary (handlers/workers log, services return errors). Sensitive data (passwords, tokens, keys) is auto-redacted.
- **HTTP clients:** NEVER create `&http.Client{}` without a `Timeout` field. All HTTP clients must have explicit timeouts (30s standard, 5s for health checks). A bare client can hang indefinitely. - **HTTP clients:** NEVER create `&http.Client{}` without a `Timeout` field. All HTTP clients must have explicit timeouts (30s standard, 5s for health checks). A bare client can hang indefinitely.

View File

@ -276,6 +276,7 @@ func main() {
blueprintService, blueprintService,
agentRegistry, agentRegistry,
projectRepo, projectRepo,
nil, // uses defaults: claudebox-0, rdev namespace
) )
// Create question service (for Foundary structured questions) // Create question service (for Foundary structured questions)

File diff suppressed because it is too large Load Diff

View File

@ -12,12 +12,20 @@ import (
"github.com/orchard9/rdev/internal/port" "github.com/orchard9/rdev/internal/port"
) )
// ArchitectServiceConfig holds configuration for the architect service.
type ArchitectServiceConfig struct {
DefaultPodName string // Default pod for agent execution (e.g., "claudebox-0")
Namespace string // Kubernetes namespace (e.g., "rdev")
}
// ArchitectService orchestrates conversational project design with Claude. // ArchitectService orchestrates conversational project design with Claude.
type ArchitectService struct { type ArchitectService struct {
conversationService *ConversationService conversationService *ConversationService
blueprintService *BlueprintService blueprintService *BlueprintService
agentRegistry port.CodeAgentRegistry agentRegistry port.CodeAgentRegistry
projectRepo port.ProjectRepository projectRepo port.ProjectRepository
defaultPodName string
namespace string
} }
// NewArchitectService creates a new architect service. // NewArchitectService creates a new architect service.
@ -26,12 +34,21 @@ func NewArchitectService(
blueprintService *BlueprintService, blueprintService *BlueprintService,
agentRegistry port.CodeAgentRegistry, agentRegistry port.CodeAgentRegistry,
projectRepo port.ProjectRepository, projectRepo port.ProjectRepository,
cfg *ArchitectServiceConfig,
) *ArchitectService { ) *ArchitectService {
if cfg == nil {
cfg = &ArchitectServiceConfig{
DefaultPodName: "claudebox-0",
Namespace: "rdev",
}
}
return &ArchitectService{ return &ArchitectService{
conversationService: conversationService, conversationService: conversationService,
blueprintService: blueprintService, blueprintService: blueprintService,
agentRegistry: agentRegistry, agentRegistry: agentRegistry,
projectRepo: projectRepo, projectRepo: projectRepo,
defaultPodName: cfg.DefaultPodName,
namespace: cfg.Namespace,
} }
} }
@ -158,6 +175,12 @@ Current conversation context:`
fullPrompt := systemPrompt + "\n\n" + prompt 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{ agentReq := &domain.AgentRequest{
Prompt: fullPrompt, Prompt: fullPrompt,
ProjectID: project.ID, ProjectID: project.ID,
@ -165,6 +188,8 @@ Current conversation context:`
Metadata: map[string]string{ Metadata: map[string]string{
"conversation_id": string(conversationID), "conversation_id": string(conversationID),
"purpose": "architect", "purpose": "architect",
"pod_name": podName,
"namespace": s.namespace,
}, },
} }
@ -242,12 +267,20 @@ Extract and return ONLY a valid JSON object with this structure:
Return ONLY the JSON, no other text.`, transcript) 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{ agentReq := &domain.AgentRequest{
Prompt: extractionPrompt, Prompt: extractionPrompt,
ProjectID: project.ID, ProjectID: project.ID,
Timeout: 2 * time.Minute, Timeout: 2 * time.Minute,
Metadata: map[string]string{ Metadata: map[string]string{
"purpose": "spec-extraction", "purpose": "spec-extraction",
"pod_name": podName,
"namespace": s.namespace,
}, },
} }
@ -283,20 +316,7 @@ Return ONLY the JSON, no other text.`, transcript)
var spec map[string]any var spec map[string]any
if err := json.Unmarshal([]byte(response), &spec); err != nil { if err := json.Unmarshal([]byte(response), &spec); err != nil {
// Fallback: create basic spec if parsing fails return nil, fmt.Errorf("parse spec extraction response: %w", err)
log := logging.FromContext(ctx)
log.Warn("failed to parse agent spec extraction, using fallback",
logging.FieldError, err,
logging.FieldOperation, "extract_blueprint_spec",
"response_length", len(response),
"message_count", len(messages),
)
spec = map[string]any{
"version": "1.0",
"generated_at": time.Now().Format(time.RFC3339),
"message_count": len(messages),
"extraction_failed": true,
}
} }
return spec, nil return spec, nil