diff options
| author | Claudomator Agent <agent@claudomator> | 2026-07-10 20:22:30 +0000 |
|---|---|---|
| committer | Claudomator Agent <agent@claudomator> | 2026-07-10 20:22:30 +0000 |
| commit | 3728fb07ddc9c3a85cf4f2152b35e090618f87db (patch) | |
| tree | 6a0f529504397dd370ba413c3b04a2f4e35470a4 /internal/storage | |
| parent | db1f7d9f18142791776a510ab4e8e28974cdcd40 (diff) | |
feat(storage): seed the builder role's system prompt on server startup
Diffstat (limited to 'internal/storage')
| -rw-r--r-- | internal/storage/seed.go | 60 | ||||
| -rw-r--r-- | internal/storage/seed_test.go | 97 |
2 files changed, 157 insertions, 0 deletions
diff --git a/internal/storage/seed.go b/internal/storage/seed.go index c2df84f..b452969 100644 --- a/internal/storage/seed.go +++ b/internal/storage/seed.go @@ -1,6 +1,8 @@ package storage import ( + "database/sql" + "errors" "os/exec" "strings" @@ -60,3 +62,61 @@ func localBareRemote(dir string) string { } return dir } + +// SeedRoleConfigs upserts default role_configs for roles that ship with no +// operator-authored configuration yet. Idempotent and non-destructive: if a +// role already has an active version -- whether seeded here on a prior +// startup, or authored later by a human via POST /api/roles/{role}/versions +// + /activate -- it is left untouched. Mirrors SeedProjects' "safe to call +// on every startup" contract. +func (s *DB) SeedRoleConfigs() error { + seeds := []struct { + role string + configJSON string + }{ + {role: "builder", configJSON: builderRoleConfigJSON}, + } + for _, seed := range seeds { + if _, err := s.GetActiveRoleConfig(seed.role); err == nil { + continue // already active; never overwrite + } else if !errors.Is(err, sql.ErrNoRows) { + return err + } + row, err := s.CreateRoleConfig(seed.role, seed.configJSON, "seed") + if err != nil { + return err + } + if err := s.ActivateRoleConfigVersion(seed.role, row.Version); err != nil { + return err + } + } + return nil +} + +// builderRoleConfigJSON is the default role.RoleConfig for the "builder" +// role -- see docs/superpowers/specs/2026-07-09-recursive-arbitrated-review-design.md, +// Implementation Order item 5: "the builder role's system prompt still +// needs to be written ... a real deliverable of implementing this design." +// Teaches the decompose-or-implement judgment and spawn_subtask's +// role/depends_on/acceptance_criteria parameters -- see +// internal/executor/agentmcp.go's spawnSubtaskInput for the exact tool +// parameter names used here (name, instructions, model, max_budget_usd, +// role, depends_on, acceptance_criteria). EscalationLadder's provider is +// "claude" to match this deployment's registered ContainerRunner (see this +// plan's Global Constraints). +const builderRoleConfigJSON = `{ + "role": "builder", + "system_prompt": "You are operating as a builder in Claudomator's recursive arbitrated-review system. Every builder-role task you touch -- whether it's a story's root task or a subtask several levels deep -- is automatically reviewed by 4 independent evaluators (quality, security, correctness, performance) and an arbitration pass once your work reaches a ready state. You do not need to ask for review, and you do not merge or accept your own work -- the orchestrator only promotes it to COMPLETED after arbitration approves it. If arbitration rejects your work, a fresh fix-attempt task is automatically spawned carrying the rejection's reasoning; whichever agent picks up that fix-attempt will see that reasoning in its own task instructions.\n\n## The decompose-or-implement judgment\n\nBefore writing any code, decide: can this task be done as one cohesive, reviewable unit of work, or does it naturally split into distinct pieces that each deserve their own focused review?\n\n- Implement directly when the task is a single cohesive change: a bug fix, a focused feature, a refactor confined to one area. Do the work, commit it, and call report_summary. Arbitrated review happens automatically once you finish.\n- Decompose when the task is large enough that bundling it into one review would blur together unrelated concerns, or when it naturally breaks into ordered or independent pieces (e.g. \"add the storage column\", then \"wire it into the API\", then \"update the UI\"). Use spawn_subtask for each piece.\n\nThis refines the general \"break work over ~3 minutes into pieces\" guidance above -- that heuristic still applies, but for a builder task specifically, decomposition is also about producing pieces that are independently reviewable, not merely independently executable.\n\n## When you decompose: use role \"builder\", not a fixed model\n\nEvery subtask you spawn that represents more implementation work -- not evaluation, not arbitration -- must be spawned with role set to \"builder\", not a fixed model. This is what makes the recursion actually recurse: a subtask spawned with role \"builder\" gets the exact same decompose-or-implement judgment and the same 4-evaluator arbitrated review your own task is getting right now, at whatever depth it sits. A subtask spawned with a fixed model instead of a role skips all of that -- only do this for something that is definitely not further build/review work; there is rarely a reason to, since builder-role dispatch already resolves an appropriate model via the role's own escalation ladder.\n\n- Use depends_on to express real ordering between subtasks (e.g. \"wire it into the API\" depends on \"add the storage column\"). Independent subtasks should not depend on each other -- false dependencies just slow the story down.\n- Use acceptance_criteria on every subtask you spawn -- a short list of concrete, checkable statements describing what that specific piece of work must satisfy. This is what the 4 evaluators and arbitration will actually judge that subtask against; a subtask with no acceptance criteria falls back to the whole story's criteria, which is usually too broad to be a meaningful check for one piece. Write criteria specific to the piece, not a restatement of the whole story.\n- Give each subtask focused, self-contained instructions -- the agent that picks it up will not see your reasoning for the overall decomposition, only what you put in instructions.\n\n## Keep decomposition shallow and purposeful\n\nDon't decompose for its own sake. A task that's already a small, single-purpose change should be implemented directly, not split into one-line subtasks -- every additional layer of decomposition adds a full evaluator-and-arbitration cycle's worth of latency and cost. Stop decomposing once a piece is small enough to implement, test, and review as one unit.", + "escalation_ladder": [ + { + "candidates": [{"provider": "claude", "model": "sonnet"}], + "selection_mode": "single", + "max_retries": 1 + }, + { + "candidates": [{"provider": "claude", "model": "opus"}], + "selection_mode": "single", + "max_retries": 0 + } + ] +}` diff --git a/internal/storage/seed_test.go b/internal/storage/seed_test.go new file mode 100644 index 0000000..cd7c8e7 --- /dev/null +++ b/internal/storage/seed_test.go @@ -0,0 +1,97 @@ +package storage + +import ( + "encoding/json" + "testing" + + "github.com/thepeterstone/claudomator/internal/role" +) + +func TestSeedRoleConfigs_CreatesAndActivatesBuilder(t *testing.T) { + db := testDB(t) + + if err := db.SeedRoleConfigs(); err != nil { + t.Fatalf("SeedRoleConfigs: %v", err) + } + + row, err := db.GetActiveRoleConfig("builder") + if err != nil { + t.Fatalf("GetActiveRoleConfig(builder): %v", err) + } + if row.Version != 1 { + t.Errorf("expected version 1, got %d", row.Version) + } + if row.ProposedBy != "seed" { + t.Errorf("expected proposed_by 'seed', got %q", row.ProposedBy) + } + + var cfg role.RoleConfig + if err := json.Unmarshal([]byte(row.ConfigJSON), &cfg); err != nil { + t.Fatalf("unmarshal seeded config: %v", err) + } + if cfg.Role != "builder" { + t.Errorf("cfg.Role = %q, want builder", cfg.Role) + } + if cfg.SystemPrompt == "" { + t.Error("cfg.SystemPrompt is empty, want the decompose-or-implement guidance") + } + if len(cfg.EscalationLadder) != 2 { + t.Fatalf("expected 2 escalation tiers, got %d", len(cfg.EscalationLadder)) + } + if cfg.EscalationLadder[0].Candidates[0].Provider != "claude" { + t.Errorf("tier 0 provider = %q, want claude", cfg.EscalationLadder[0].Candidates[0].Provider) + } +} + +func TestSeedRoleConfigs_Idempotent_DoesNotDuplicateOrOverwrite(t *testing.T) { + db := testDB(t) + + if err := db.SeedRoleConfigs(); err != nil { + t.Fatalf("first SeedRoleConfigs: %v", err) + } + if err := db.SeedRoleConfigs(); err != nil { + t.Fatalf("second SeedRoleConfigs: %v", err) + } + + versions, err := db.ListRoleConfigVersions("builder") + if err != nil { + t.Fatalf("ListRoleConfigVersions: %v", err) + } + if len(versions) != 1 { + t.Fatalf("expected exactly 1 version after calling SeedRoleConfigs twice, got %d", len(versions)) + } +} + +func TestSeedRoleConfigs_DoesNotOverwriteHumanCustomizedVersion(t *testing.T) { + db := testDB(t) + + // A human (or the retro role's propose_role_config tool) already + // created and activated their own version before the seed ever ran. + custom, err := db.CreateRoleConfig("builder", `{"role":"builder","system_prompt":"custom prompt"}`, "human") + if err != nil { + t.Fatalf("CreateRoleConfig: %v", err) + } + if err := db.ActivateRoleConfigVersion("builder", custom.Version); err != nil { + t.Fatalf("ActivateRoleConfigVersion: %v", err) + } + + if err := db.SeedRoleConfigs(); err != nil { + t.Fatalf("SeedRoleConfigs: %v", err) + } + + row, err := db.GetActiveRoleConfig("builder") + if err != nil { + t.Fatalf("GetActiveRoleConfig: %v", err) + } + if row.ProposedBy != "human" { + t.Errorf("active role config proposed_by = %q, want unchanged 'human' -- seed must not overwrite a human-authored active version", row.ProposedBy) + } + + versions, err := db.ListRoleConfigVersions("builder") + if err != nil { + t.Fatalf("ListRoleConfigVersions: %v", err) + } + if len(versions) != 1 { + t.Fatalf("expected exactly 1 version (seed must not create a competing draft), got %d", len(versions)) + } +} |
