feat(analyst): этапы задачи — аналитик раскладывает на steps с критериями готовности, dev идёт по ним
Some checks failed
CI / test (push) Failing after 1m52s
CI / build-and-package (amd64, linux) (push) Failing after 1m3s
CI / build-and-package (amd64, windows) (push) Successful in 30s

This commit is contained in:
ki.sagidullin
2026-08-24 18:05:06 +05:00
parent 6546f18348
commit 6c0b903794
10 changed files with 286 additions and 50 deletions

View File

@@ -3,6 +3,8 @@ package worker
import (
"strings"
"text/template"
"github.com/kamelion/ratatoskr-go/internal/storage"
)
// devPromptTemplate — промпт для dev-агента при запуске задачи.
@@ -20,6 +22,12 @@ var devPromptTemplate = template.Must(template.New("dev").Parse(`Ты — dev-а
{{if .Why}}Зачем: {{.Why}}{{end}}
{{if .AC}}Критерии готовности:
{{.AC}}{{end}}
{{if .Steps}}
**Этапы (выполняй по порядку, у каждого свой критерий готовности):**
{{- range .Steps}}
{{.Title}}{{if .AC}} — готово, когда: {{.AC}}{{end}}
{{- end}}
{{end}}
**Инструкции:**
1. Рабочий каталог — общий корень, в котором лежат все репозитории по именам.
@@ -38,6 +46,7 @@ type DevPromptData struct {
Repos []string
Why string
AC string
Steps []storage.Step
// ReviewFeedback — замечания ревьюера при повторном прогоне dev
// (не пусто → dev должен исправить именно это).
@@ -124,4 +133,4 @@ func reviewFeedbackList(branch string, comments []string) []string {
return nil
}
return []string{s}
}
}

View File

@@ -255,6 +255,7 @@ func (w *Worker) runTask(ctx context.Context, task *storage.Task) (err error) {
Repos: repos,
Why: task.Why,
AC: task.AC,
Steps: task.Steps,
Branch: branch,
ReviewFeedback: reviewFeedbackList(branch, feedback),
}

View File

@@ -725,6 +725,7 @@ func TestWorkerBadStatus(t *testing.T) {
}
}
// TestWorkerPromptRendered — dev-промпт собирается из полей задачи.
func TestWorkerPromptRendered(t *testing.T) {
s := setupWorkerDB(t)
task := createReadyTask(t, s, "prompt-test")
@@ -758,6 +759,63 @@ func TestWorkerPromptRendered(t *testing.T) {
}
}
// TestWorkerPromptIncludesSteps — этапы задачи попадают в dev-промпт по порядку
// с критериями готовности.
func TestWorkerPromptIncludesSteps(t *testing.T) {
s := setupWorkerDB(t)
task := createReadyTask(t, s, "steps-test")
ctx := context.Background()
task.Steps = []storage.Step{
{Title: "Модель", AC: "операции готовы"},
{Title: "UI"},
}
if err := s.UpdateTask(ctx, task); err != nil {
t.Fatalf("update steps: %v", err)
}
w := &Worker{
Store: s,
Runner: &mockRunnerWorker{result: &opencode.Result{RC: 0, Stdout: "ok", SessionID: "s"}},
Worktree: t.TempDir(),
}
seedFakeRepo(t, w.Worktree, "steps-test")
_ = w.runTask(ctx, task)
traces, err := s.GetTraces(ctx, task.ID)
if err != nil {
t.Fatalf("get traces: %v", err)
}
tr := traces[0]
for _, want := range []string{"Этапы", "Модель", "операции готовы", "UI"} {
if !strings.Contains(tr.Prompt, want) {
t.Errorf("dev-промпт не содержит %q", want)
}
}
}
// TestRenderDevPromptSteps — прямой рендер dev-промпта с этапами.
func TestRenderDevPromptSteps(t *testing.T) {
prompt, err := RenderDevPrompt(DevPromptData{
Title: "Калькулятор",
AC: "работает",
Steps: []storage.Step{
{Title: "Модель", AC: "операции готовы"},
{Title: "UI"},
},
Branch: "feat/abc",
})
if err != nil {
t.Fatalf("render: %v", err)
}
for _, want := range []string{"Этапы", "Модель — готово, когда: операции готовы", "UI"} {
if !strings.Contains(prompt, want) {
t.Errorf("prompt не содержит %q", want)
}
}
}
func TestFeatureBranchName(t *testing.T) {
cases := []struct {
tag string