summaryrefslogtreecommitdiff
path: root/internal/storage/seed.go
blob: e7a0ba0ad8594f1d5f15fdd585f821d7f2f56913 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package storage

import (
	"database/sql"
	"errors"
	"os/exec"
	"strings"

	"github.com/thepeterstone/claudomator/internal/task"
)

// SeedProjects upserts the default project registry on startup.
func (s *DB) SeedProjects() error {
	projects := []*task.Project{
		{
			ID:           "claudomator",
			Name:         "claudomator",
			LocalPath:    "/workspace/claudomator",
			RemoteURL:    localBareRemote("/workspace/claudomator"),
			Type:         "web",
			DeployScript: "/workspace/claudomator/scripts/deploy",
		},
		{
			ID:        "nav",
			Name:      "nav",
			LocalPath: "/workspace/nav",
			RemoteURL: localBareRemote("/workspace/nav"),
			Type:      "android",
		},
		{
			ID:           "doot",
			Name:         "doot",
			LocalPath:    "/workspace/doot",
			RemoteURL:    localBareRemote("/workspace/doot"),
			Type:         "web",
			DeployScript: "/workspace/doot/scripts/deploy",
		},
		{
			ID:        "modal-shell",
			Name:      "modal-shell",
			LocalPath: "/workspace/modal-shell",
			RemoteURL: localBareRemote("/workspace/modal-shell"),
			Type:      "web",
		},
	}
	for _, p := range projects {
		if err := s.UpsertProject(p); err != nil {
			return err
		}
	}
	return nil
}

// localBareRemote returns the URL of the "local" git remote for dir,
// falling back to dir itself if the remote is not configured.
func localBareRemote(dir string) string {
	out, err := exec.Command("git", "-C", dir, "remote", "get-url", "local").Output()
	if err == nil {
		if url := strings.TrimSpace(string(out)); url != "" {
			return url
		}
	}
	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},
		{role: "planner", configJSON: plannerRoleConfigJSON},
	}
	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
    }
  ]
}`

// plannerRoleConfigJSON is the default role.RoleConfig for the "planner"
// role -- claudomator's arbitration role (see StoryOrchestrator.ensureArbitration
// in internal/scheduler/story_orchestrator.go). Added 2026-07-11 after a
// live production run demonstrated a real gap: with no system prompt at
// all, an arbitration agent completed without ever calling report_verdict,
// and finalizeArbitration's then-fail-open default (no verdict = approve)
// silently shipped work an evaluator had already flagged as factually
// wrong. finalizeArbitration was made fail-closed in the same change (no
// verdict now means rejection, not approval) -- this system prompt is the
// other half of that fix: telling the agent the tool exists and that
// calling it is mandatory, not just "hoping" a fail-closed default alone is
// enough forcing function. See report_verdict's schema in
// internal/executor/agentmcp.go's reportVerdictInput (approved bool,
// reasoning string).
const plannerRoleConfigJSON = `{
  "role": "planner",
  "system_prompt": "You are operating as the arbitration role in Claudomator's recursive arbitrated-review system. You have been dispatched because a builder-role task's work has already been reviewed by 4 independent evaluators (quality, security, correctness, performance). Your job is to read each evaluator's findings and render the final verdict: does this work meet its acceptance criteria and ship, or does it need to go back for a fix?\n\n## You MUST call report_verdict before finishing\n\nThis is not optional. If you finish without calling report_verdict, the system treats that the same as an explicit rejection -- NOT an approval. There is no safe default here: skipping this call always sends the work back for a fix, even if it was actually fine. So every single time: read the evaluators' findings, form a judgment, and call report_verdict with approved (true or false) and reasoning before you finish.\n\n## How to read the evaluators' findings\n\nEach evaluator recorded its assessment as that task's summary and/or events -- your own task instructions name each evaluator task to check. Read all 4 before deciding. Weigh their findings against the acceptance criteria you were given, not against an abstract standard of perfection. A nitpick that doesn't affect correctness, security, or whether the work does what it claims is not grounds for rejection; a finding that the work is factually wrong, broken, or misses a stated acceptance criterion is.\n\n## Approve or reject\n\n- approved: true -- the work meets its acceptance criteria. The builder task is promoted to COMPLETED.\n- approved: false -- reject, with reasoning specific enough that whoever picks up the resulting fix-attempt knows exactly what to fix. A fresh fix-attempt task is automatically spawned carrying your reasoning verbatim.\n\nYour reasoning field is read by the next attempt at this exact task if you reject it -- write it as instructions for that future agent, not as a report to a human.",
  "escalation_ladder": [
    {
      "candidates": [{"provider": "claude", "model": "sonnet"}],
      "selection_mode": "single",
      "max_retries": 1
    },
    {
      "candidates": [{"provider": "claude", "model": "opus"}],
      "selection_mode": "single",
      "max_retries": 0
    }
  ]
}`