diff --git a/internal/opencode/pool.go b/internal/opencode/pool.go index 1537447..3618c35 100644 --- a/internal/opencode/pool.go +++ b/internal/opencode/pool.go @@ -34,12 +34,6 @@ type Pool struct { segs map[string]*Server // dir → сервер (root тоже здесь) used map[int]bool // занятые порты next int // следующий кандидат порта - - // externs — внешние (уже запущенные) серверы, зарегистрированные как - // префикс каталога: Ensure(dir) отдаёт такой сервер и для подкаталогов - // зарегистрированного каталога (используется тестами для эмуляции serve, - // покрывающего всё дерево worktree). - externs map[string]*Server // dir → внешний сервер } // NewPool создаёт пул. rootDir помечен как служебный (не снимается ReleaseTask). @@ -98,18 +92,11 @@ func (p *Pool) EnsureRoot(ctx context.Context) error { // Возвращает сервер; root-сервер для rootDir возвращается как есть. func (p *Pool) Ensure(ctx context.Context, dir string) (*Server, error) { p.mu.Lock() + if s, ok := p.segs[dir]; ok { + p.mu.Unlock() + return s, nil + } abs := filepath.Clean(dir) - if s, ok := p.segs[abs]; ok { - p.mu.Unlock() - return s, nil - } - // Внешний сервер-префикс покрывает дерево каталога (тестовая эмуляция): - // подкаталог обслуживается тем же сервером, spawn не нужен. - if s := p.externalFor(abs); s != nil { - p.segs[abs] = s - p.mu.Unlock() - return s, nil - } s := &Server{ Bin: p.Bin, DBPath: p.DBPath, @@ -142,31 +129,9 @@ func (p *Pool) RegisterExternal(dir, url string) { p.mu.Lock() defer p.mu.Unlock() abs := filepath.Clean(dir) - s := &Server{URL: url, Host: p.Host, PollInterval: 0} - p.segs[abs] = s - if p.externs == nil { - p.externs = map[string]*Server{} - } - p.externs[abs] = s + p.segs[abs] = &Server{URL: url, Host: p.Host, PollInterval: 0} if p.rootDir != "" && abs == p.rootDir { - p.root = s - } -} - -// externalFor возвращает внешний сервер-префикс для dir: самого каталога или -// его родителя (подкаталоги зарегистрированного каталога обслуживаются тем же -// внешним сервером). Вызывается под блокировкой p.mu. -func (p *Pool) externalFor(dir string) *Server { - cur := dir - for { - if s, ok := p.externs[cur]; ok { - return s - } - parent := filepath.Dir(cur) - if parent == cur { - return nil - } - cur = parent + p.root = p.segs[abs] } } diff --git a/internal/worker/postmortem.go b/internal/worker/postmortem.go index 3a1c9f3..ae7e3ea 100644 --- a/internal/worker/postmortem.go +++ b/internal/worker/postmortem.go @@ -142,25 +142,13 @@ func (w *Worker) hasPostMortemTrace(ctx context.Context, taskID int64) bool { // // Статус задачи НЕ меняется (failed/timeout остаётся достигнутым); собственные // сбои постмортема не влияют на исход задачи — только логируются. -// postMortem запускает постмортем-агент для задачи, завершившейся failed/timeout: -// собирает сессии dev/reviewer из трасс, даёт агенту анализ, сохраняет результат -// как trace agent=postmortem и уведомляет владельца задачи резюме. -// -// projectDir — каталог основного репозитория (тот же serve, где жили сессии -// dev/reviewer); fallback на w.Worktree, если папка репо недоступна. -// -// Статус задачи НЕ меняется (failed/timeout остаётся достигнутым); собственные -// сбои постмортема не влияют на исход задачи — только логируются. -func (w *Worker) postMortem(ctx context.Context, task *storage.Task, projectDir string) { +func (w *Worker) postMortem(ctx context.Context, task *storage.Task) { if w.Store == nil || w.Runner == nil { return } if w.hasPostMortemTrace(ctx, task.ID) { return } - if projectDir == "" { - projectDir = w.Worktree - } traces, err := w.Store.GetTraces(ctx, task.ID) if err != nil { @@ -184,7 +172,7 @@ func (w *Worker) postMortem(ctx context.Context, task *storage.Task, projectDir if tr.SessionID == "" { continue } - tx, tErr := w.Runner.SessionMessages(w.runCtx(ctx, task.ID), projectDir, tr.SessionID) + tx, tErr := w.Runner.SessionMessages(w.runCtx(ctx, task.ID), w.Worktree, tr.SessionID) if tErr != nil { log.Printf("worker: task %d: постмортем: транскрипт %s (%s): %v", task.ID, tr.Agent, tr.SessionID, tErr) continue @@ -216,7 +204,7 @@ func (w *Worker) postMortem(ctx context.Context, task *storage.Task, projectDir return } - res, rErr := w.Runner.Run(w.runCtx(ctx, task.ID), prompt, projectDir, postMortemAgent, "") + res, rErr := w.Runner.Run(w.runCtx(ctx, task.ID), prompt, w.Worktree, postMortemAgent, "") if rErr != nil { log.Printf("worker: task %d: постмортем: запуск: %v", task.ID, rErr) w.finalizeTrace(ctx, traceID, storage.TraceFailed, rErr.Error()) @@ -246,9 +234,9 @@ func (w *Worker) postMortem(ctx context.Context, task *storage.Task, projectDir // postMortemAfter — defer-хук из runTask: запускает постмортем, если задача // завершилась failed/timeout. Собственные ошибки постмортема не мешают // исходному результату задачи (возвращаемый *error только читается). -func (w *Worker) postMortemAfter(ctx context.Context, task *storage.Task, projectDir string, _ *error) { +func (w *Worker) postMortemAfter(ctx context.Context, task *storage.Task, _ *error) { if task.Status != storage.StatusFailed && task.Status != storage.StatusTimeout { return } - w.postMortem(ctx, task, projectDir) + w.postMortem(ctx, task) } diff --git a/internal/worker/prompt.go b/internal/worker/prompt.go index cdda571..21ed5d0 100644 --- a/internal/worker/prompt.go +++ b/internal/worker/prompt.go @@ -14,14 +14,11 @@ var devPromptTemplate = template.Must(template.New("dev").Parse(`Ты — dev-а {{if .Title}}Название: {{.Title}}{{end}} {{if .Goal}}Цель: {{.Goal}}{{end}} {{if .Repos}} -Репозитории: +Репозитории (доступны как подпапки текущего каталога): {{- range .Repos}} - {{.}} {{- end}} {{end}} -{{if .Primary}} -Основной репозиторий (рабочий каталог этой сессии): {{.Primary}} -{{end}} {{if .Why}}Зачем: {{.Why}}{{end}} {{if .AC}}Критерии готовности: {{.AC}}{{end}} @@ -33,8 +30,8 @@ var devPromptTemplate = template.Must(template.New("dev").Parse(`Ты — dev-а {{end}} **Инструкции:** -1. Рабочий каталог — сам основной репозиторий ({{.Primary}}): правь файлы в нём прямо, относительными путями (./...). -{{if .Others}} Связанные репозитории задачи лежат рядом, в ../имя_репо/ (здесь cwd — основной репозиторий): меняй их согласованно, если этого требует задача.{{else}} Работа ведётся только в нём — связанных репозиториев у задачи нет.{{end}} +1. Рабочий каталог — общий корень, в котором лежат все репозитории по именам. + Правь файлы внутри нужного репозитория (./имя_репо/...). Связанные репозитории меняй согласованно. 2. Напиши код, реализующий задачу. 3. Убедись, что все acceptance criteria выполнены. 4. Пользуйся встроенными инструментами opencode (чтение файлов, поиск, редактирование). @@ -47,14 +44,9 @@ type DevPromptData struct { Title string Goal string Repos []string - // Primary — основной (первый) репозиторий задачи: рабочий каталог сессии - // dev — это именно его папка (сервер opencode стартует из неё). - Primary string - // Others — остальные репозитории задачи (доступны рядом как ../имя_репо/). - Others []string - Why string - AC string - Steps []storage.Step + Why string + AC string + Steps []storage.Step // ReviewFeedback — замечания ревьюера при повторном прогоне dev // (не пусто → dev должен исправить именно это). diff --git a/internal/worker/worker.go b/internal/worker/worker.go index fb24eba..bd1f110 100644 --- a/internal/worker/worker.go +++ b/internal/worker/worker.go @@ -201,12 +201,7 @@ func (w *Worker) pollAndDispatch(ctx context.Context) error { func (w *Worker) runTask(ctx context.Context, task *storage.Task) (err error) { // 0. постмортем-анализ: если задача завершилась failed/timeout — после // выхода из runTask (всех путей) запускаем анализ сессий dev/reviewer. - // projectDir (каталог основного репозитория) объявляем заранее — defer - // захватывает переменную, её значение подставится к моменту возврата. - var projectDir string - defer func() { - w.postMortemAfter(ctx, task, projectDir, &err) - }() + defer w.postMortemAfter(ctx, task, &err) // 1. проверяем статус if task.Status != storage.StatusApproved { @@ -246,12 +241,8 @@ func (w *Worker) runTask(ctx context.Context, task *storage.Task) (err error) { } } - // 3. cwd — папка основного репозитория: сервер opencode для работы стартует - // из самого репозитория (подхватывает .serena, экономит токены на поиск - // файлов из корня worktree). Служебный root-сервер в worktree остаётся - // для аналитика; пул поднимает свой serve на каждый репозиторий работы. - cwd := w.repoDirOf(repos[0]) - projectDir = cwd + // 3. cwd — общий каталог (вариант A: один dev видит все репозитории). + cwd := w.Worktree // Цикл dev → review, до maxReviewIterations. var feedback []string @@ -262,8 +253,6 @@ func (w *Worker) runTask(ctx context.Context, task *storage.Task) (err error) { Title: task.Title, Goal: task.Goal, Repos: repos, - Primary: repos[0], - Others: repos[1:], Why: task.Why, AC: task.AC, Steps: task.Steps,