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:
@@ -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": "",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 // ответ пользователю
|
||||
|
||||
Reference in New Issue
Block a user