feat: фаза ready — черновик готов как есть, без требований к изменённым полям
Аналитик теперь может вернуть phase=ready, когда черновик уже полный и менять нечего. Раньше модель не могла это выразить и возвращала пустой propose, который валидатор резал A3 (нет изменённых полей). - analyst: case ready в validateResponse (без требований к полям) - core: propose и ready обрабатываются одинаково (applyDraft + E1 + ready) - prompt/analyst.md: контракт фаз обновлён (ask|propose|ready|abort) - тест TestDecideReady + README: раздел фаз аналитика
This commit is contained in:
17
README.md
17
README.md
@@ -129,6 +129,23 @@ update:
|
|||||||
| `/status` | версия бинаря + есть ли доступное обновление |
|
| `/status` | версия бинаря + есть ли доступное обновление |
|
||||||
| `/help` | справка по всем командам |
|
| `/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
|
## Автообновление из Gitea Packages
|
||||||
|
|
||||||
Бинарь умеет сам себя обновлять из generic-пакета в Gitea. Модель:
|
Бинарь умеет сам себя обновлять из generic-пакета в Gitea. Модель:
|
||||||
|
|||||||
@@ -16,15 +16,19 @@ mode: primary
|
|||||||
1. Пока неясны цель, репозиторий или критерии готовности — задай до 3 уточняющих вопросов
|
1. Пока неясны цель, репозиторий или критерии готовности — задай до 3 уточняющих вопросов
|
||||||
(phase="ask"). Не задавай лишних вопросов, если поле уже заполнено.
|
(phase="ask"). Не задавай лишних вопросов, если поле уже заполнено.
|
||||||
2. Когда данных достаточно (понятны цель, репозиторий, ≥2–3 проверяемых критерия) —
|
2. Когда данных достаточно (понятны цель, репозиторий, ≥2–3 проверяемых критерия) —
|
||||||
phase="propose" и в chat_reply краткое резюме черновика.
|
phase="propose" и в chat_reply краткое резюме черновика. Заполни те поля, которые
|
||||||
3. Если тема явно не про разработку кода или не подходит для агента-кодера — phase="abort"
|
меняешь; остальные — пустой строкой.
|
||||||
|
3. Если черновик УЖЕ полный и готов как есть, менять нечего — phase="ready": все поля
|
||||||
|
изменений пустыми, в chat_reply — резюме и подтверждение готовности. Задачу можно
|
||||||
|
отправлять агенту-кодеру.
|
||||||
|
4. Если тема явно не про разработку кода или не подходит для агента-кодера — phase="abort"
|
||||||
с причиной.
|
с причиной.
|
||||||
4. Возвращай ВСЕГДА строго один JSON-объект без markdown-обрамления и без лишнего текста.
|
5. Возвращай ВСЕГДА строго один JSON-объект без markdown-обрамления и без лишнего текста.
|
||||||
5. Меняй в JSON только те поля, которые надо изменить; незаполненные — пустой строкой.
|
6. Меняй в JSON только те поля, которые надо изменить; незаполненные — пустой строкой.
|
||||||
|
|
||||||
Формат ответа:
|
Формат ответа:
|
||||||
{
|
{
|
||||||
"phase": "ask|propose|abort",
|
"phase": "ask|propose|ready|abort",
|
||||||
"title": "",
|
"title": "",
|
||||||
"goal": "",
|
"goal": "",
|
||||||
"repo": "",
|
"repo": "",
|
||||||
|
|||||||
@@ -173,6 +173,8 @@ func validateResponse(ar *AnalystResponse) error {
|
|||||||
if ar.Title == "" && ar.Goal == "" && len(ar.Repos) == 0 && ar.Why == "" && ar.AC == "" {
|
if ar.Title == "" && ar.Goal == "" && len(ar.Repos) == 0 && ar.Why == "" && ar.AC == "" {
|
||||||
return fmt.Errorf("phase=propose, но нет ни одного изменённого поля")
|
return fmt.Errorf("phase=propose, но нет ни одного изменённого поля")
|
||||||
}
|
}
|
||||||
|
case "ready":
|
||||||
|
// черновик уже полный и готов как есть — изменённых полей НЕ требуется
|
||||||
case "abort":
|
case "abort":
|
||||||
// abort_reason — не обязателен, но желателен
|
// abort_reason — не обязателен, но желателен
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -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) {
|
func TestDecodeFail(t *testing.T) {
|
||||||
a := &Analyst{Runner: &mockRunner{result: &opencode.Result{
|
a := &Analyst{Runner: &mockRunner{result: &opencode.Result{
|
||||||
RC: 0,
|
RC: 0,
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ var promptTemplate = template.Must(template.New("analyst").Parse(`Ты — ан
|
|||||||
| why | зачем это нужно, контекст |
|
| why | зачем это нужно, контекст |
|
||||||
| ac | acceptance criteria — конкретный результат, что считается готовым |
|
| 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}}
|
{{- if .Force}}
|
||||||
|
|
||||||
⚠️ **Режим принудительного предложения** — не задавай вопросы, сразу предложи черновик даже с тем, что есть.
|
⚠️ **Режим принудительного предложения** — не задавай вопросы, сразу предложи черновик даже с тем, что есть.
|
||||||
@@ -46,7 +54,7 @@ var promptTemplate = template.Must(template.New("analyst").Parse(`Ты — ан
|
|||||||
|
|
||||||
**Ответь строго JSON-объектом, без лишнего текста:**
|
**Ответь строго JSON-объектом, без лишнего текста:**
|
||||||
{
|
{
|
||||||
"phase": "ask|propose|abort",
|
"phase": "ask|propose|ready|abort",
|
||||||
"title": "название (только если меняешь)",
|
"title": "название (только если меняешь)",
|
||||||
"goal": "цель (только если меняешь)",
|
"goal": "цель (только если меняешь)",
|
||||||
"repos": ["имя_репо_1", "имя_репо_2"],
|
"repos": ["имя_репо_1", "имя_репо_2"],
|
||||||
|
|||||||
@@ -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
|
return Result{Reply: reply, Action: "drop", TaskID: task.ID, Status: task.Status}, nil
|
||||||
|
|
||||||
case "propose":
|
case "propose", "ready":
|
||||||
// применяем черновик
|
// применяем черновик (для ready — текущий, без изменений)
|
||||||
applyDraft(task, decision.Draft)
|
applyDraft(task, decision.Draft)
|
||||||
// E1: propose без репозиториев → остаёмся в сборе, просим уточнить.
|
// E1: propose/ready без репозиториев → остаёмся в сборе, просим уточнить.
|
||||||
if len(task.EffectiveRepos()) == 0 {
|
if len(task.EffectiveRepos()) == 0 {
|
||||||
task.Status = storage.StatusCollecting
|
task.Status = storage.StatusCollecting
|
||||||
if err := c.Store.UpdateTask(ctx, task); err != nil {
|
if err := c.Store.UpdateTask(ctx, task); err != nil {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type Message struct {
|
|||||||
|
|
||||||
// Decision — вердикт аналитика.
|
// Decision — вердикт аналитика.
|
||||||
type Decision struct {
|
type Decision struct {
|
||||||
Phase string // "ask" | "propose" | "abort"
|
Phase string // "ask" | "propose" | "ready" | "abort"
|
||||||
Draft storage.Task // обновлённые поля черновика
|
Draft storage.Task // обновлённые поля черновика
|
||||||
Questions []string // вопросы для phase=ask
|
Questions []string // вопросы для phase=ask
|
||||||
ChatReply string // ответ пользователю
|
ChatReply string // ответ пользователю
|
||||||
|
|||||||
Reference in New Issue
Block a user