Add council review start command
This commit is contained in:
@@ -462,6 +462,16 @@ CREATE TABLE IF NOT EXISTS council_runs (
|
|||||||
updated_at TEXT NOT NULL
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS council_inputs (
|
||||||
|
run_id TEXT PRIMARY KEY,
|
||||||
|
prompt TEXT NOT NULL DEFAULT '',
|
||||||
|
target_file TEXT NOT NULL DEFAULT '',
|
||||||
|
repo_path TEXT NOT NULL DEFAULT '',
|
||||||
|
target_task_id TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS council_reviewers (
|
CREATE TABLE IF NOT EXISTS council_reviewers (
|
||||||
run_id TEXT NOT NULL,
|
run_id TEXT NOT NULL,
|
||||||
reviewer_role TEXT NOT NULL,
|
reviewer_role TEXT NOT NULL,
|
||||||
|
|||||||
@@ -28,9 +28,10 @@ As of now:
|
|||||||
- `orch` can create runs, gate tasks through dependencies, dispatch work through `inbox`, reconcile worker thread state back into task state, answer blocked tasks, retry or reassign work, cancel tasks or runs, clean attempt worktrees, and create per-attempt Git worktrees during strict dispatch
|
- `orch` can create runs, gate tasks through dependencies, dispatch work through `inbox`, reconcile worker thread state back into task state, answer blocked tasks, retry or reassign work, cancel tasks or runs, clean attempt worktrees, and create per-attempt Git worktrees during strict dispatch
|
||||||
- `orch dispatch` now supports `--repo-path`, `--workspace-root`, and `--strict-worktree`, auto-enables strict worktree mode for code-like tasks inferred from task metadata, resolves committed base revisions, records workspace metadata on attempts, and writes that metadata into inbox task payloads
|
- `orch dispatch` now supports `--repo-path`, `--workspace-root`, and `--strict-worktree`, auto-enables strict worktree mode for code-like tasks inferred from task metadata, resolves committed base revisions, records workspace metadata on attempts, and writes that metadata into inbox task payloads
|
||||||
- `orch wait` now blocks on run-scoped task events and reconciles inbox state while polling so leader waits can wake on worker progress without manual sleep loops
|
- `orch wait` now blocks on run-scoped task events and reconciles inbox state while polling so leader waits can wake on worker progress without manual sleep loops
|
||||||
- automated integration tests now cover the main `orch` scheduler slice, including dependency gating, dispatch, blocked-answer flow, retry, reassign, cancel, cleanup, strict worktree creation, automatic code-task worktree enablement, dirty-repo rejection rules, and wait wake/timeout behavior
|
- `orch council start` now creates a dedicated council run, persists council target input metadata, and dispatches the three fixed reviewer roles through the existing scheduler
|
||||||
|
- automated integration tests now cover the main `orch` scheduler slice, including dependency gating, dispatch, blocked-answer flow, retry, reassign, cancel, cleanup, strict worktree creation, automatic code-task worktree enablement, dirty-repo rejection rules, wait wake/timeout behavior, and council start dispatch
|
||||||
|
|
||||||
This means the project now has a working `orch` core scheduler with automatic worktree selection for code-like tasks, strict worktree-backed dispatch, and the main leader-side control loop, and is ready for council workflows.
|
This means the project now has a working `orch` core scheduler with automatic worktree selection for code-like tasks, strict worktree-backed dispatch, the main leader-side control loop, and the first council workflow slice.
|
||||||
|
|
||||||
## Source Of Truth
|
## Source Of Truth
|
||||||
|
|
||||||
@@ -73,8 +74,9 @@ Current implementation status:
|
|||||||
- `Milestone 4: Orch Core Scheduling` is complete for the current non-worktree scheduler scope
|
- `Milestone 4: Orch Core Scheduling` is complete for the current non-worktree scheduler scope
|
||||||
- `Milestone 5: Strict Worktree Support` is complete
|
- `Milestone 5: Strict Worktree Support` is complete
|
||||||
- `Milestone 6: Waiting Primitives` is complete
|
- `Milestone 6: Waiting Primitives` is complete
|
||||||
|
- `Milestone 7: Council Review` is partially complete through `orch council start`
|
||||||
|
|
||||||
The next practical coding target is `Milestone 7: Council Review`.
|
The next practical coding target is the next `Milestone 7` slice: `orch council wait`.
|
||||||
|
|
||||||
### Milestone 1: Go Skeleton
|
### Milestone 1: Go Skeleton
|
||||||
|
|
||||||
@@ -331,15 +333,32 @@ Definition of done:
|
|||||||
- tally grouped recommendations into `consensus`, `majority`, and `minority`
|
- tally grouped recommendations into `consensus`, `majority`, and `minority`
|
||||||
- produce stable JSON and a markdown report artifact
|
- produce stable JSON and a markdown report artifact
|
||||||
|
|
||||||
|
Status:
|
||||||
|
|
||||||
|
- partially complete through `orch council start`
|
||||||
|
|
||||||
|
Completed so far:
|
||||||
|
|
||||||
|
- council-specific storage now includes run metadata, reviewer assignment rows, reviewer findings/groups tables, and persisted council input references
|
||||||
|
- `orch council start`
|
||||||
|
- council start creates a dedicated run, stores council target input metadata, creates reviewer tasks `CR1` through `CR3`, and dispatches the fixed reviewer roles `architecture-reviewer`, `implementation-reviewer`, and `risk-reviewer`
|
||||||
|
- CLI integration tests cover council start dispatch and metadata persistence
|
||||||
|
|
||||||
|
Remaining:
|
||||||
|
|
||||||
|
- `orch council wait`
|
||||||
|
- `orch council tally`
|
||||||
|
- `orch council report`
|
||||||
|
|
||||||
## Immediate Next Task
|
## Immediate Next Task
|
||||||
|
|
||||||
If a new agent is taking over now, the next concrete step should be:
|
If a new agent is taking over now, the next concrete step should be:
|
||||||
|
|
||||||
1. start `Milestone 7: Council Review`
|
1. continue `Milestone 7: Council Review` with `orch council wait`
|
||||||
2. define the persisted storage shape for grouped reviewer recommendations and council wake/tally state
|
2. define the council completion check that determines when all reviewer outputs are ready for tally
|
||||||
3. keep the authored inbox test-plan set in `docs/tests/inbox/` synchronized if CLI behavior changes during further `orch` work
|
3. keep the authored inbox test-plan set in `docs/tests/inbox/` synchronized if CLI behavior changes during further `orch` work
|
||||||
|
|
||||||
The inbox implementation and its human-readable test-plan set are already in place, and `orch` now supports the main scheduler loop plus automatic worktree selection for code-like tasks, so the next meaningful project step is the council workflow layer.
|
The inbox implementation and its human-readable test-plan set are already in place, and `orch` now supports the main scheduler loop plus the first council workflow slice, so the next meaningful project step is finishing council wait, tally, and report.
|
||||||
|
|
||||||
## Recommended Driver Choices
|
## Recommended Driver Choices
|
||||||
|
|
||||||
@@ -365,6 +384,7 @@ Completed so far:
|
|||||||
- orch strict worktree creation and dirty-repo policy coverage
|
- orch strict worktree creation and dirty-repo policy coverage
|
||||||
- orch wait wake and timeout coverage
|
- orch wait wake and timeout coverage
|
||||||
- orch retry, reassign, cancel, and cleanup coverage
|
- orch retry, reassign, cancel, and cleanup coverage
|
||||||
|
- orch council start dispatch and persistence coverage
|
||||||
|
|
||||||
Still recommended before the codebase grows too much:
|
Still recommended before the codebase grows too much:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Orch Council Start
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- `completed`
|
||||||
|
|
||||||
|
## Owner
|
||||||
|
|
||||||
|
- codex
|
||||||
|
|
||||||
|
## Started At
|
||||||
|
|
||||||
|
- `2026-03-19`
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
- implement the first Milestone 7 slice by adding council input storage and `orch council start`
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- persist council run metadata plus target input references
|
||||||
|
- add `orch council start` to create a council run and dispatch three reviewer tasks
|
||||||
|
- add integration tests for council start dispatch behavior
|
||||||
|
- update the implementation roadmap and archive this workstream when complete
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [x] inspect council docs, schema, and existing orch dispatch primitives
|
||||||
|
- [x] implement council storage and `orch council start`
|
||||||
|
- [x] add integration coverage for council start
|
||||||
|
- [x] run `go test ./...`
|
||||||
|
- [x] update `docs/implementation-roadmap.md`
|
||||||
|
- [x] archive this roadmap with a completion summary
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `docs/roadmaps/archive/orch-council-start.md`
|
||||||
|
- `docs/implementation-roadmap.md`
|
||||||
|
- `internal/db/schema/*.sql`
|
||||||
|
- `internal/store/council.go`
|
||||||
|
- `internal/cli/orch/root.go`
|
||||||
|
- `internal/cli/orch/council*.go`
|
||||||
|
- `internal/cli/orch/integration_test.go`
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- treat `orch council start` as creating a dedicated run id rather than attaching to a pre-existing ordinary run
|
||||||
|
- persist target prompt/file/repo/task references in a separate council input table instead of overloading the existing `runs` table
|
||||||
|
|
||||||
|
## Blockers
|
||||||
|
|
||||||
|
- none
|
||||||
|
|
||||||
|
## Next Step
|
||||||
|
|
||||||
|
- continue Milestone 7 with `orch council wait`, then add tally and report on top of the persisted reviewer data
|
||||||
|
|
||||||
|
## Completion Summary
|
||||||
|
|
||||||
|
- council input references are now persisted separately from council run metadata
|
||||||
|
- `orch council start` creates a dedicated council run, dispatches the three fixed reviewer roles, and records reviewer assignment rows for downstream wait/tally/report steps
|
||||||
|
- integration tests now cover council start dispatch behavior and stored council metadata
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package orch
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
func newCouncilCmd(root *rootOptions) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "council",
|
||||||
|
Short: "Council review workflow commands",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(newCouncilStartCmd(root))
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package orch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"ai-workflow-skill/internal/protocol"
|
||||||
|
"ai-workflow-skill/internal/store"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type councilStartOptions struct {
|
||||||
|
runID string
|
||||||
|
target string
|
||||||
|
targetFile string
|
||||||
|
repoPath string
|
||||||
|
targetTaskID string
|
||||||
|
targetType string
|
||||||
|
mode string
|
||||||
|
outputMode string
|
||||||
|
onlyUnanimous bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCouncilStartCmd(root *rootOptions) *cobra.Command {
|
||||||
|
opts := &councilStartOptions{}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "start",
|
||||||
|
Short: "Create and dispatch a three-reviewer council run",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
|
||||||
|
sqlDB, err := openOrchDB(ctx, root.dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sqlDB.Close()
|
||||||
|
|
||||||
|
result, err := store.NewOrchStore(sqlDB).StartCouncil(ctx, store.CouncilStartInput{
|
||||||
|
RunID: opts.runID,
|
||||||
|
Target: opts.target,
|
||||||
|
TargetFile: opts.targetFile,
|
||||||
|
RepoPath: opts.repoPath,
|
||||||
|
TargetTaskID: opts.targetTaskID,
|
||||||
|
TargetType: opts.targetType,
|
||||||
|
Mode: opts.mode,
|
||||||
|
OutputMode: opts.outputMode,
|
||||||
|
OnlyUnanimous: opts.onlyUnanimous,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := protocol.Success{
|
||||||
|
OK: true,
|
||||||
|
Command: "council start",
|
||||||
|
Data: map[string]any{
|
||||||
|
"run_id": result.Run.RunID,
|
||||||
|
"mode": result.Run.Mode,
|
||||||
|
"target_type": result.Run.TargetType,
|
||||||
|
"output": result.Run.OutputMode,
|
||||||
|
"only_unanimous": result.Run.OnlyUnanimous,
|
||||||
|
"target": result.Input,
|
||||||
|
"reviewers": result.Reviewers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if root.json {
|
||||||
|
return protocol.WriteJSON(cmd.OutOrStdout(), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintf(cmd.OutOrStdout(), "started council run %s with %d reviewers\n", result.Run.RunID, len(result.Reviewers))
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID")
|
||||||
|
cmd.Flags().StringVar(&opts.target, "target", "", "Inline target prompt")
|
||||||
|
cmd.Flags().StringVar(&opts.targetFile, "target-file", "", "Optional target context file")
|
||||||
|
cmd.Flags().StringVar(&opts.repoPath, "repo-path", "", "Optional repository path for review context")
|
||||||
|
cmd.Flags().StringVar(&opts.targetTaskID, "task-id", "", "Optional related task ID")
|
||||||
|
cmd.Flags().StringVar(&opts.targetType, "target-type", "mixed", "Target type: text, repo, or mixed")
|
||||||
|
cmd.Flags().StringVar(&opts.mode, "mode", "brainstorm", "Council mode: brainstorm or review")
|
||||||
|
cmd.Flags().StringVar(&opts.outputMode, "output", "both", "Output mode: markdown, json, or both")
|
||||||
|
cmd.Flags().BoolVar(&opts.onlyUnanimous, "only-unanimous", false, "Show only unanimous recommendations in downstream report defaults")
|
||||||
|
_ = cmd.MarkFlagRequired("run")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package orch
|
package orch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -1396,3 +1397,146 @@ func TestOrchCleanupRemovesCompletedWorktree(t *testing.T) {
|
|||||||
t.Fatalf("expected cleaned worktree path to be removed, err=%v", err)
|
t.Fatalf("expected cleaned worktree path to be removed, err=%v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOrchCouncilStartDispatchesThreeReviewers(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dbPath := filepath.Join(t.TempDir(), "coord.db")
|
||||||
|
|
||||||
|
startOut := runOrchCommand(
|
||||||
|
t,
|
||||||
|
"--db", dbPath,
|
||||||
|
"--json",
|
||||||
|
"council", "start",
|
||||||
|
"--run", "council_blog_001",
|
||||||
|
"--target", "Review the current blog architecture and propose optimizations.",
|
||||||
|
"--target-type", "mixed",
|
||||||
|
"--output", "both",
|
||||||
|
)
|
||||||
|
|
||||||
|
var startResp map[string]any
|
||||||
|
mustDecodeJSON(t, startOut, &startResp)
|
||||||
|
if got := nestedString(t, startResp, "data", "run_id"); got != "council_blog_001" {
|
||||||
|
t.Fatalf("expected council run id, got %q", got)
|
||||||
|
}
|
||||||
|
if got := nestedString(t, startResp, "data", "mode"); got != "brainstorm" {
|
||||||
|
t.Fatalf("expected default council mode brainstorm, got %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
reviewers := nestedArray(t, startResp, "data", "reviewers")
|
||||||
|
if len(reviewers) != 3 {
|
||||||
|
t.Fatalf("expected three council reviewers, got %#v", reviewers)
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDB, err := openOrchDB(t.Context(), dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open orch db: %v", err)
|
||||||
|
}
|
||||||
|
defer sqlDB.Close()
|
||||||
|
|
||||||
|
var (
|
||||||
|
mode string
|
||||||
|
targetType string
|
||||||
|
outputMode string
|
||||||
|
onlyUnanimous int
|
||||||
|
)
|
||||||
|
if err := sqlDB.QueryRowContext(
|
||||||
|
t.Context(),
|
||||||
|
`SELECT mode, target_type, output_mode, only_unanimous
|
||||||
|
FROM council_runs
|
||||||
|
WHERE run_id = ?`,
|
||||||
|
"council_blog_001",
|
||||||
|
).Scan(&mode, &targetType, &outputMode, &onlyUnanimous); err != nil {
|
||||||
|
t.Fatalf("query council_runs: %v", err)
|
||||||
|
}
|
||||||
|
if mode != "brainstorm" || targetType != "mixed" || outputMode != "both" || onlyUnanimous != 0 {
|
||||||
|
t.Fatalf("unexpected council run metadata: mode=%q targetType=%q outputMode=%q onlyUnanimous=%d", mode, targetType, outputMode, onlyUnanimous)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
prompt string
|
||||||
|
targetFile string
|
||||||
|
repoPath string
|
||||||
|
targetTaskID string
|
||||||
|
)
|
||||||
|
if err := sqlDB.QueryRowContext(
|
||||||
|
t.Context(),
|
||||||
|
`SELECT prompt, target_file, repo_path, target_task_id
|
||||||
|
FROM council_inputs
|
||||||
|
WHERE run_id = ?`,
|
||||||
|
"council_blog_001",
|
||||||
|
).Scan(&prompt, &targetFile, &repoPath, &targetTaskID); err != nil {
|
||||||
|
t.Fatalf("query council_inputs: %v", err)
|
||||||
|
}
|
||||||
|
if prompt == "" || targetFile != "" || repoPath != "" || targetTaskID != "" {
|
||||||
|
t.Fatalf("unexpected council input row: prompt=%q targetFile=%q repoPath=%q targetTaskID=%q", prompt, targetFile, repoPath, targetTaskID)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := sqlDB.QueryContext(
|
||||||
|
t.Context(),
|
||||||
|
`SELECT reviewer_role, task_id, status
|
||||||
|
FROM council_reviewers
|
||||||
|
WHERE run_id = ?
|
||||||
|
ORDER BY reviewer_role ASC`,
|
||||||
|
"council_blog_001",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("query council_reviewers: %v", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var reviewerRows int
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
reviewerRole string
|
||||||
|
taskID string
|
||||||
|
status string
|
||||||
|
)
|
||||||
|
if err := rows.Scan(&reviewerRole, &taskID, &status); err != nil {
|
||||||
|
t.Fatalf("scan council reviewer row: %v", err)
|
||||||
|
}
|
||||||
|
reviewerRows++
|
||||||
|
if status != "dispatched" {
|
||||||
|
t.Fatalf("expected council reviewer status dispatched, got %q for %s", status, reviewerRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
var worktreePath sql.NullString
|
||||||
|
if err := sqlDB.QueryRowContext(
|
||||||
|
t.Context(),
|
||||||
|
`SELECT a.worktree_path
|
||||||
|
FROM task_attempts a
|
||||||
|
WHERE a.run_id = ? AND a.task_id = ? AND a.attempt_no = 1`,
|
||||||
|
"council_blog_001",
|
||||||
|
taskID,
|
||||||
|
).Scan(&worktreePath); err != nil {
|
||||||
|
t.Fatalf("query council attempt worktree path for %s: %v", taskID, err)
|
||||||
|
}
|
||||||
|
if worktreePath.Valid && worktreePath.String != "" {
|
||||||
|
t.Fatalf("expected council reviewer task %s to avoid worktree allocation, got %q", taskID, worktreePath.String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
t.Fatalf("iterate council reviewers: %v", err)
|
||||||
|
}
|
||||||
|
if reviewerRows != 3 {
|
||||||
|
t.Fatalf("expected three stored council reviewers, got %d", reviewerRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
statusOut := runOrchCommand(
|
||||||
|
t,
|
||||||
|
"--db", dbPath,
|
||||||
|
"--json",
|
||||||
|
"status",
|
||||||
|
"--run", "council_blog_001",
|
||||||
|
)
|
||||||
|
|
||||||
|
var statusResp map[string]any
|
||||||
|
mustDecodeJSON(t, statusOut, &statusResp)
|
||||||
|
if got := nestedString(t, statusResp, "data", "run", "status"); got != "running" {
|
||||||
|
t.Fatalf("expected council run status running, got %q", got)
|
||||||
|
}
|
||||||
|
tasks := nestedArray(t, statusResp, "data", "tasks")
|
||||||
|
if len(tasks) != 3 {
|
||||||
|
t.Fatalf("expected three council tasks, got %#v", tasks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ func NewRootCmd() *cobra.Command {
|
|||||||
cmd.AddCommand(newReassignCmd(opts))
|
cmd.AddCommand(newReassignCmd(opts))
|
||||||
cmd.AddCommand(newCancelCmd(opts))
|
cmd.AddCommand(newCancelCmd(opts))
|
||||||
cmd.AddCommand(newCleanupCmd(opts))
|
cmd.AddCommand(newCleanupCmd(opts))
|
||||||
|
cmd.AddCommand(newCouncilCmd(opts))
|
||||||
cmd.AddCommand(newBlockedCmd(opts))
|
cmd.AddCommand(newBlockedCmd(opts))
|
||||||
cmd.AddCommand(newAnswerCmd(opts))
|
cmd.AddCommand(newAnswerCmd(opts))
|
||||||
cmd.AddCommand(newStatusCmd(opts))
|
cmd.AddCommand(newStatusCmd(opts))
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS council_inputs (
|
||||||
|
run_id TEXT PRIMARY KEY,
|
||||||
|
prompt TEXT NOT NULL DEFAULT '',
|
||||||
|
target_file TEXT NOT NULL DEFAULT '',
|
||||||
|
repo_path TEXT NOT NULL DEFAULT '',
|
||||||
|
target_task_id TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
@@ -0,0 +1,444 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user