feat: множественные репозитории (Repos) и клонирование в воркере
All checks were successful
CI / test (push) Successful in 46s
CI / build-and-package (amd64, darwin) (push) Successful in 36s
CI / build-and-package (amd64, linux) (push) Successful in 37s
CI / build-and-package (amd64, windows) (push) Successful in 44s
CI / build-and-package (arm64, darwin) (push) Successful in 35s
CI / build-and-package (arm64, linux) (push) Successful in 36s

- Task.Repos []string (XML-колонка repos, обратная совместимость с repo)
- config: блок git {base_url, token}
- аналитик: ответ repos[], шаблон показывает список
- core: propose без repos → возврат в сбор (E1)
- worker вариант A: один dev из общего cwd, prepareRepos клонирует
  недостающие репо (git clone), validateRepoName (E3), ErrRepoNotGit (E4)
- ошибки E1-E4 в worker/errors.go
This commit is contained in:
Hermes
2026-08-16 09:18:15 +05:00
parent 7be75fe14f
commit bd3d825738
14 changed files with 346 additions and 71 deletions

View File

@@ -318,8 +318,17 @@ 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":
// применяем черновик и переходим в ready
// применяем черновик
applyDraft(task, decision.Draft)
// E1: propose без репозиториев → остаёмся в сборе, просим уточнить.
if len(task.EffectiveRepos()) == 0 {
task.Status = storage.StatusCollecting
if err := c.Store.UpdateTask(ctx, task); err != nil {
return Result{}, err
}
reply := decChatReply(decision, "Укажи, в каком репозитории(ях) вести работу.")
return Result{Reply: reply, Action: "send", TaskID: task.ID, Status: task.Status}, nil
}
task.Status = storage.StatusReady
if err := c.Store.UpdateTask(ctx, task); err != nil {
return Result{}, err
@@ -353,6 +362,10 @@ func applyDraft(task *storage.Task, dec storage.Task) {
if dec.Repo != "" {
task.Repo = dec.Repo
}
if len(dec.Repos) > 0 {
task.Repos = dec.Repos
task.Repo = strings.Join(dec.Repos, ",")
}
if dec.Why != "" {
task.Why = dec.Why
}
@@ -361,6 +374,14 @@ func applyDraft(task *storage.Task, dec storage.Task) {
}
}
// decChatReply возвращает ChatReply из вердикта или переданный дефолт.
func decChatReply(dec Decision, fallback string) string {
if dec.ChatReply != "" {
return dec.ChatReply
}
return fallback
}
// buildAskReply формирует ответ с вопросами.
func buildAskReply(dec Decision, max int) string {
reply := dec.ChatReply
@@ -391,8 +412,8 @@ func formatSummary(t storage.Task) string {
if t.Title != "" {
b.WriteString("**Название:** " + t.Title + "\n")
}
if t.Repo != "" {
b.WriteString("**Репозиторий:** " + t.Repo + "\n")
if len(t.EffectiveRepos()) > 0 {
b.WriteString("**Репозитории:** " + strings.Join(t.EffectiveRepos(), ", ") + "\n")
}
if t.Goal != "" {
b.WriteString("**Цель:** " + t.Goal + "\n")

View File

@@ -148,7 +148,7 @@ func TestAbortReturnsDrop(t *testing.T) {
func TestConsentInReady(t *testing.T) {
c, ctx, store := setupCore(t, func(ctx context.Context, history []Message, draft storage.Task, force bool) (Decision, error) {
return Decision{Phase: "propose", Draft: storage.Task{Title: "X"}}, nil
return Decision{Phase: "propose", Draft: storage.Task{Title: "X", Repos: []string{"repo-x"}}}, nil
})
id := mkTask(t, store, ctx, "u1")
@@ -166,7 +166,7 @@ func TestEditInReadyGoesCollecting(t *testing.T) {
var calls int
c, ctx, store := setupCore(t, func(ctx context.Context, history []Message, draft storage.Task, force bool) (Decision, error) {
calls++
return Decision{Phase: "propose", Draft: storage.Task{Title: "X"}}, nil
return Decision{Phase: "propose", Draft: storage.Task{Title: "X", Repos: []string{"repo-x"}}}, nil
})
id := mkTask(t, store, ctx, "u1")