package store import ( "context" "errors" "fmt" "strings" ) var councilReviewerRoles = []string{ "architecture-reviewer", "implementation-reviewer", "risk-reviewer", } type CouncilRun struct { RunID string `json:"run_id"` Mode string `json:"mode"` TargetType string `json:"target_type"` OutputMode string `json:"output_mode"` OnlyUnanimous bool `json:"only_unanimous"` } type CouncilInput struct { RunID string `json:"run_id"` Prompt string `json:"prompt,omitempty"` TargetFile string `json:"target_file,omitempty"` RepoPath string `json:"repo_path,omitempty"` TargetTaskID string `json:"task_id,omitempty"` } type CouncilReviewer struct { ReviewerRole string `json:"reviewer_role"` TaskID string `json:"task_id"` Status string `json:"status"` } type CouncilStartInput struct { RunID string Target string TargetFile string RepoPath string TargetTaskID string TargetType string Mode string OutputMode string OnlyUnanimous bool } type CouncilStartResult struct { Run CouncilRun `json:"run"` Input CouncilInput `json:"input"` Reviewers []CouncilReviewer `json:"reviewers"` } func (s *OrchStore) StartCouncil(ctx context.Context, input CouncilStartInput) (CouncilStartResult, error) { runID := strings.TrimSpace(input.RunID) if runID == "" { return CouncilStartResult{}, fmt.Errorf("%w: run id is required", ErrInvalidInput) } councilInput, err := normalizeCouncilInput(input) if err != nil { return CouncilStartResult{}, err } councilRun, err := normalizeCouncilRun(input) if err != nil { return CouncilStartResult{}, err } now := nowUTC() tx, err := s.db.BeginTx(ctx, nil) if err != nil { return CouncilStartResult{}, fmt.Errorf("begin council start transaction: %w", err) } defer tx.Rollback() if _, err := selectRun(ctx, tx, runID); err == nil { return CouncilStartResult{}, fmt.Errorf("%w: run %s already exists", ErrInvalidState, runID) } else if !errors.Is(err, ErrRunNotFound) { return CouncilStartResult{}, err } goal := buildCouncilRunGoal(councilInput) summary := buildCouncilRunSummary(councilRun, councilInput) _, err = tx.ExecContext( ctx, `INSERT INTO runs (run_id, goal, summary, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, runID, goal, summary, "active", formatTime(now), formatTime(now), ) if err != nil { return CouncilStartResult{}, fmt.Errorf("insert council run into runs: %w", err) } if err := insertEvent(ctx, tx, eventInput{ RunID: runID, Source: "orch", EventType: "run_initialized", Summary: summary, PayloadJSON: marshalJSON(map[string]any{"goal": goal, "summary": summary}), CreatedAt: now, }); err != nil { return CouncilStartResult{}, err } _, err = tx.ExecContext( ctx, `INSERT INTO council_runs ( run_id, mode, target_type, output_mode, only_unanimous, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?)`, runID, councilRun.Mode, councilRun.TargetType, councilRun.OutputMode, boolToInt(councilRun.OnlyUnanimous), formatTime(now), formatTime(now), ) if err != nil { return CouncilStartResult{}, fmt.Errorf("insert council run metadata: %w", err) } _, err = tx.ExecContext( ctx, `INSERT INTO council_inputs ( run_id, prompt, target_file, repo_path, target_task_id, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?)`, runID, councilInput.Prompt, councilInput.TargetFile, councilInput.RepoPath, councilInput.TargetTaskID, formatTime(now), formatTime(now), ) if err != nil { return CouncilStartResult{}, fmt.Errorf("insert council input metadata: %w", err) } reviewers := make([]CouncilReviewer, 0, len(councilReviewerRoles)) for i, reviewerRole := range councilReviewerRoles { taskID := fmt.Sprintf("CR%d", i+1) task := Task{ RunID: runID, TaskID: taskID, Title: buildCouncilTaskTitle(reviewerRole), Summary: buildCouncilTaskSummary(reviewerRole), Status: "ready", DefaultTo: reviewerRole, Priority: "normal", AcceptanceJSON: []byte(buildCouncilTaskAcceptanceJSON(councilRun, councilInput, reviewerRole)), CreatedAt: now, UpdatedAt: now, } _, err = tx.ExecContext( ctx, `INSERT INTO tasks ( run_id, task_id, title, summary, status, default_to, priority, acceptance_json, latest_attempt_no, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`, task.RunID, task.TaskID, task.Title, task.Summary, task.Status, nullIfEmpty(task.DefaultTo), task.Priority, string(task.AcceptanceJSON), formatTime(task.CreatedAt), formatTime(task.UpdatedAt), ) if err != nil { return CouncilStartResult{}, fmt.Errorf("insert council reviewer task: %w", err) } if err := insertEvent(ctx, tx, eventInput{ RunID: runID, TaskID: taskID, Source: "orch", EventType: "task_added", Summary: task.Title, PayloadJSON: marshalJSON(map[string]any{"title": task.Title, "priority": task.Priority}), CreatedAt: now, }); err != nil { return CouncilStartResult{}, err } if err := insertEvent(ctx, tx, eventInput{ RunID: runID, TaskID: taskID, Source: "orch", EventType: "task_ready", Summary: task.Title, PayloadJSON: marshalJSON(map[string]any{"task_id": taskID}), CreatedAt: now, }); err != nil { return CouncilStartResult{}, err } dispatchResult, finalizeWorkspace, err := s.dispatchTaskTx( ctx, tx, task, reviewerRole, buildCouncilTaskBody(councilRun, councilInput, reviewerRole), "", nil, now, ) if err != nil { return CouncilStartResult{}, err } defer finalizeWorkspace(false) _, err = tx.ExecContext( ctx, `INSERT INTO council_reviewers (run_id, reviewer_role, task_id, status) VALUES (?, ?, ?, ?)`, runID, reviewerRole, taskID, dispatchResult.Task.Status, ) if err != nil { return CouncilStartResult{}, fmt.Errorf("insert council reviewer row: %w", err) } reviewers = append(reviewers, CouncilReviewer{ ReviewerRole: reviewerRole, TaskID: taskID, Status: dispatchResult.Task.Status, }) } if err := insertEvent(ctx, tx, eventInput{ RunID: runID, Source: "orch", EventType: "council_started", Summary: "council reviewers dispatched", PayloadJSON: marshalJSON(map[string]any{ "mode": councilRun.Mode, "target_type": councilRun.TargetType, "output_mode": councilRun.OutputMode, "only_unanimous": councilRun.OnlyUnanimous, "reviewers": reviewers, }), CreatedAt: now, }); err != nil { return CouncilStartResult{}, err } if err := updateRunAggregateStatus(ctx, tx, runID, now); err != nil { return CouncilStartResult{}, err } if err := tx.Commit(); err != nil { return CouncilStartResult{}, fmt.Errorf("commit council start transaction: %w", err) } return CouncilStartResult{ Run: councilRun, Input: councilInput, Reviewers: reviewers, }, nil } func normalizeCouncilRun(input CouncilStartInput) (CouncilRun, error) { mode := defaultString(strings.TrimSpace(input.Mode), "brainstorm") switch mode { case "brainstorm", "review": default: return CouncilRun{}, fmt.Errorf("%w: mode must be brainstorm or review", ErrInvalidInput) } targetType := defaultString(strings.TrimSpace(input.TargetType), "mixed") switch targetType { case "text", "repo", "mixed": default: return CouncilRun{}, fmt.Errorf("%w: target-type must be text, repo, or mixed", ErrInvalidInput) } outputMode := defaultString(strings.TrimSpace(input.OutputMode), "both") switch outputMode { case "markdown", "json", "both": default: return CouncilRun{}, fmt.Errorf("%w: output must be markdown, json, or both", ErrInvalidInput) } return CouncilRun{ RunID: strings.TrimSpace(input.RunID), Mode: mode, TargetType: targetType, OutputMode: outputMode, OnlyUnanimous: input.OnlyUnanimous, }, nil } func normalizeCouncilInput(input CouncilStartInput) (CouncilInput, error) { result := CouncilInput{ RunID: strings.TrimSpace(input.RunID), Prompt: strings.TrimSpace(input.Target), TargetFile: strings.TrimSpace(input.TargetFile), RepoPath: strings.TrimSpace(input.RepoPath), TargetTaskID: strings.TrimSpace(input.TargetTaskID), } if result.Prompt == "" && result.TargetFile == "" && result.RepoPath == "" && result.TargetTaskID == "" { return CouncilInput{}, fmt.Errorf("%w: at least one of target, target-file, repo-path, or task-id is required", ErrInvalidInput) } return result, nil } func buildCouncilRunGoal(input CouncilInput) string { switch { case input.Prompt != "": return "Council review: " + truncateSingleLine(input.Prompt, 80) case input.TargetTaskID != "": return "Council review for task " + input.TargetTaskID case input.TargetFile != "": return "Council review for " + input.TargetFile case input.RepoPath != "": return "Council review for repo " + input.RepoPath default: return "Council review" } } func buildCouncilRunSummary(run CouncilRun, input CouncilInput) string { return fmt.Sprintf("%s council (%s)", run.Mode, run.TargetType) } func buildCouncilTaskTitle(reviewerRole string) string { switch reviewerRole { case "architecture-reviewer": return "Council architecture review" case "implementation-reviewer": return "Council implementation review" case "risk-reviewer": return "Council risk review" default: return "Council review" } } func buildCouncilTaskSummary(reviewerRole string) string { switch reviewerRole { case "architecture-reviewer": return "Review the target for architecture, boundaries, and interfaces" case "implementation-reviewer": return "Review the target for simplicity, maintainability, and practicality" case "risk-reviewer": return "Review the target for regressions, correctness, and operability risks" default: return "Review the target" } } func buildCouncilTaskAcceptanceJSON(run CouncilRun, input CouncilInput, reviewerRole string) string { return marshalJSON(map[string]any{ "mode": "analysis", "council": map[string]any{ "reviewer_role": reviewerRole, "council_mode": run.Mode, "target_type": run.TargetType, "output_mode": run.OutputMode, "only_unanimous": run.OnlyUnanimous, "target": map[string]any{ "prompt": input.Prompt, "target_file": input.TargetFile, "repo_path": input.RepoPath, "task_id": input.TargetTaskID, }, "response_format": map[string]any{ "reviewer_role": reviewerRole, "findings": []map[string]any{ { "title": "string", "summary": "string", "proposal": "string", "rationale": "string", "confidence": "low|medium|high", "tags": []string{}, "target_refs": map[string]any{}, }, }, }, }, }) } func buildCouncilTaskBody(run CouncilRun, input CouncilInput, reviewerRole string) string { parts := []string{ fmt.Sprintf("Reviewer role: %s", reviewerRole), fmt.Sprintf("Council mode: %s", run.Mode), fmt.Sprintf("Target type: %s", run.TargetType), "Analyze the target from your assigned reviewer perspective.", "Return structured findings with title, summary, proposal, rationale, confidence, tags, and optional target references.", } if input.Prompt != "" { parts = append(parts, "", "Prompt:", input.Prompt) } if input.TargetFile != "" { parts = append(parts, "", "Target file:", input.TargetFile) } if input.RepoPath != "" { parts = append(parts, "", "Repo path:", input.RepoPath) } if input.TargetTaskID != "" { parts = append(parts, "", "Related task id:", input.TargetTaskID) } return strings.Join(parts, "\n") } func truncateSingleLine(value string, maxLen int) string { value = strings.TrimSpace(value) value = strings.ReplaceAll(value, "\n", " ") value = strings.ReplaceAll(value, "\r", " ") value = strings.Join(strings.Fields(value), " ") if maxLen <= 0 || len(value) <= maxLen { return value } if maxLen <= 3 { return value[:maxLen] } return value[:maxLen-3] + "..." } func boolToInt(value bool) int { if value { return 1 } return 0 }