From 46065f84fd79f23401bab08585c5b8ab30676bb7 Mon Sep 17 00:00:00 2001 From: Hermes Date: Mon, 17 Aug 2026 14:07:05 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=84=D0=B0=D0=B7=D0=B0=20ready=20?= =?UTF-8?q?=E2=80=94=20=D1=87=D0=B5=D1=80=D0=BD=D0=BE=D0=B2=D0=B8=D0=BA=20?= =?UTF-8?q?=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=20=D0=BA=D0=B0=D0=BA=20=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D1=8C,=20=D0=B1=D0=B5=D0=B7=20=D1=82=D1=80=D0=B5?= =?UTF-8?q?=D0=B1=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B9=20=D0=BA=20=D0=B8?= =?UTF-8?q?=D0=B7=D0=BC=D0=B5=D0=BD=D1=91=D0=BD=D0=BD=D1=8B=D0=BC=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8F=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Аналитик теперь может вернуть phase=ready, когда черновик уже полный и менять нечего. Раньше модель не могла это выразить и возвращала пустой propose, который валидатор резал A3 (нет изменённых полей). - analyst: case ready в validateResponse (без требований к полям) - core: propose и ready обрабатываются одинаково (applyDraft + E1 + ready) - prompt/analyst.md: контракт фаз обновлён (ask|propose|ready|abort) - тест TestDecideReady + README: раздел фаз аналитика --- README.md | 17 +++++++++++++++++ internal/agents/analyst.md | 14 +++++++++----- internal/analyst/analyst.go | 2 ++ internal/analyst/analyst_test.go | 17 +++++++++++++++++ internal/analyst/prompt.go | 10 +++++++++- internal/core/core.go | 6 +++--- internal/core/decider.go | 2 +- 7 files changed, 58 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 528ba12..b5fbc51 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,23 @@ update: | `/status` | версия бинаря + есть ли доступное обновление | | `/help` | справка по всем командам | +## Фазы аналитика + +Аналитик (`internal/analyst`) возвращает JSON-вердикт с полем `phase`: + +| phase | Смысл | Требования валидатора | +|---|---|---| +| `ask` | данных не хватает — задаёт уточняющие вопросы | есть `chat_reply` или `questions` | +| `propose` | черновик достаточно заполнен, вносятся правки | хотя бы одно изменённое поле (`title`/`goal`/`repos`/`why`/`ac`) | +| `ready` | черновик уже полный и готов как есть — менять нечего | изменённых полей не требуется | +| `abort` | тема не про код / не подходит | опционально `abort_reason` | + +`ready` был добавлен как явная фаза для случая, когда аналитик видит полностью готовый +черновик и просто запускает задачу агенту-кодеру (раньше модель не могла это выразить и +возвращала пустой `propose`, который валидатор резал как «нет изменённых полей»). +В `internal/core` фазы `propose` и `ready` обрабатываются одинаково (применить черновик, +проверить репозитории, поставить `ready` и отдать резюме). + ## Автообновление из Gitea Packages Бинарь умеет сам себя обновлять из generic-пакета в Gitea. Модель: diff --git a/internal/agents/analyst.md b/internal/agents/analyst.md index 1ba69da..e336458 100644 --- a/internal/agents/analyst.md +++ b/internal/agents/analyst.md @@ -16,15 +16,19 @@ mode: primary 1. Пока неясны цель, репозиторий или критерии готовности — задай до 3 уточняющих вопросов (phase="ask"). Не задавай лишних вопросов, если поле уже заполнено. 2. Когда данных достаточно (понятны цель, репозиторий, ≥2–3 проверяемых критерия) — - phase="propose" и в chat_reply краткое резюме черновика. -3. Если тема явно не про разработку кода или не подходит для агента-кодера — phase="abort" + phase="propose" и в chat_reply краткое резюме черновика. Заполни те поля, которые + меняешь; остальные — пустой строкой. +3. Если черновик УЖЕ полный и готов как есть, менять нечего — phase="ready": все поля + изменений пустыми, в chat_reply — резюме и подтверждение готовности. Задачу можно + отправлять агенту-кодеру. +4. Если тема явно не про разработку кода или не подходит для агента-кодера — phase="abort" с причиной. -4. Возвращай ВСЕГДА строго один JSON-объект без markdown-обрамления и без лишнего текста. -5. Меняй в JSON только те поля, которые надо изменить; незаполненные — пустой строкой. +5. Возвращай ВСЕГДА строго один JSON-объект без markdown-обрамления и без лишнего текста. +6. Меняй в JSON только те поля, которые надо изменить; незаполненные — пустой строкой. Формат ответа: { - "phase": "ask|propose|abort", + "phase": "ask|propose|ready|abort", "title": "", "goal": "", "repo": "", diff --git a/internal/analyst/analyst.go b/internal/analyst/analyst.go index 417c6f8..befe9ae 100644 --- a/internal/analyst/analyst.go +++ b/internal/analyst/analyst.go @@ -173,6 +173,8 @@ func validateResponse(ar *AnalystResponse) error { if ar.Title == "" && ar.Goal == "" && len(ar.Repos) == 0 && ar.Why == "" && ar.AC == "" { return fmt.Errorf("phase=propose, но нет ни одного изменённого поля") } + case "ready": + // черновик уже полный и готов как есть — изменённых полей НЕ требуется case "abort": // abort_reason — не обязателен, но желателен default: diff --git a/internal/analyst/analyst_test.go b/internal/analyst/analyst_test.go index 4aa4ae0..f49d754 100644 --- a/internal/analyst/analyst_test.go +++ b/internal/analyst/analyst_test.go @@ -87,6 +87,23 @@ func TestDecideAbort(t *testing.T) { } } +func TestDecideReady(t *testing.T) { + // ready с пустыми изменёнными полями — ВАЛИДНО (черновик готов как есть) + a := &Analyst{Runner: &mockRunner{result: &opencode.Result{ + RC: 0, + Stdout: `{"type":"text","part":{"text":"{\"phase\":\"ready\",\"chat_reply\":\"Черновик готов, запускаю.\"}"}}`, + }}, Worktree: "/tmp"} + + history := []core.Message{{Role: "user", Content: "Сделай калькулятор"}} + dec, err := a.Decide(context.Background(), history, storage.Task{}, false) + if err != nil { + t.Fatalf("Decide err: %v", err) + } + if dec.Phase != "ready" { + t.Errorf("Phase = %q, want ready", dec.Phase) + } +} + func TestDecodeFail(t *testing.T) { a := &Analyst{Runner: &mockRunner{result: &opencode.Result{ RC: 0, diff --git a/internal/analyst/prompt.go b/internal/analyst/prompt.go index b8c155f..a065266 100644 --- a/internal/analyst/prompt.go +++ b/internal/analyst/prompt.go @@ -24,6 +24,14 @@ var promptTemplate = template.Must(template.New("analyst").Parse(`Ты — ан | why | зачем это нужно, контекст | | ac | acceptance criteria — конкретный результат, что считается готовым | +Изменяй в JSON только те поля, которые надо поменять; что менять не надо — пустой строкой. + +**Фаза ответа (phase):** +- `ask` — данных не хватает: задай вопросы (до 3), черновик не меняешь. +- `propose` — черновик ДОСТАТОЧЕН и ты ВНОСИШЬ правки: заполни изменяемые поля (title/goal/repos/why/ac), в chat_reply — краткое резюме. +- `ready` — черновик уже ПОЛНЫЙ и готов как есть, менять НЕЧЕГО: phase="ready", все поля изменений оставь пустыми, в chat_reply — резюме и подтверждение готовности. Задачу можно отправлять агенту-кодеру. +- `abort` — тема не про код / не подходит: phase="abort" с abort_reason. + {{- if .Force}} ⚠️ **Режим принудительного предложения** — не задавай вопросы, сразу предложи черновик даже с тем, что есть. @@ -46,7 +54,7 @@ var promptTemplate = template.Must(template.New("analyst").Parse(`Ты — ан **Ответь строго JSON-объектом, без лишнего текста:** { - "phase": "ask|propose|abort", + "phase": "ask|propose|ready|abort", "title": "название (только если меняешь)", "goal": "цель (только если меняешь)", "repos": ["имя_репо_1", "имя_репо_2"], diff --git a/internal/core/core.go b/internal/core/core.go index 8f571b8..ba12b72 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -333,10 +333,10 @@ func (c *Core) runDecide(ctx context.Context, task *storage.Task, force bool) (R } return Result{Reply: reply, Action: "drop", TaskID: task.ID, Status: task.Status}, nil - case "propose": - // применяем черновик + case "propose", "ready": + // применяем черновик (для ready — текущий, без изменений) applyDraft(task, decision.Draft) - // E1: propose без репозиториев → остаёмся в сборе, просим уточнить. + // E1: propose/ready без репозиториев → остаёмся в сборе, просим уточнить. if len(task.EffectiveRepos()) == 0 { task.Status = storage.StatusCollecting if err := c.Store.UpdateTask(ctx, task); err != nil { diff --git a/internal/core/decider.go b/internal/core/decider.go index 9be1ef7..235e096 100644 --- a/internal/core/decider.go +++ b/internal/core/decider.go @@ -15,7 +15,7 @@ type Message struct { // Decision — вердикт аналитика. type Decision struct { - Phase string // "ask" | "propose" | "abort" + Phase string // "ask" | "propose" | "ready" | "abort" Draft storage.Task // обновлённые поля черновика Questions []string // вопросы для phase=ask ChatReply string // ответ пользователю