Revert "feat(opencode): рабочий serve для dev стартует из папки репозитория; пул покрывает внешним сервером подкаталоги"
Some checks failed
CI / test (push) Failing after 1m53s
CI / build-and-package (amd64, linux) (push) Failing after 1m3s
CI / build-and-package (amd64, windows) (push) Successful in 34s

This reverts commit 2005421739.
This commit is contained in:
ki.sagidullin
2026-08-24 23:37:28 +05:00
parent 75132bc5f5
commit 4301589429
4 changed files with 20 additions and 86 deletions

View File

@@ -34,12 +34,6 @@ type Pool struct {
segs map[string]*Server // dir → сервер (root тоже здесь) segs map[string]*Server // dir → сервер (root тоже здесь)
used map[int]bool // занятые порты used map[int]bool // занятые порты
next int // следующий кандидат порта next int // следующий кандидат порта
// externs — внешние (уже запущенные) серверы, зарегистрированные как
// префикс каталога: Ensure(dir) отдаёт такой сервер и для подкаталогов
// зарегистрированного каталога (используется тестами для эмуляции serve,
// покрывающего всё дерево worktree).
externs map[string]*Server // dir → внешний сервер
} }
// NewPool создаёт пул. rootDir помечен как служебный (не снимается ReleaseTask). // NewPool создаёт пул. rootDir помечен как служебный (не снимается ReleaseTask).
@@ -98,18 +92,11 @@ func (p *Pool) EnsureRoot(ctx context.Context) error {
// Возвращает сервер; root-сервер для rootDir возвращается как есть. // Возвращает сервер; root-сервер для rootDir возвращается как есть.
func (p *Pool) Ensure(ctx context.Context, dir string) (*Server, error) { func (p *Pool) Ensure(ctx context.Context, dir string) (*Server, error) {
p.mu.Lock() p.mu.Lock()
if s, ok := p.segs[dir]; ok {
p.mu.Unlock()
return s, nil
}
abs := filepath.Clean(dir) 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{ s := &Server{
Bin: p.Bin, Bin: p.Bin,
DBPath: p.DBPath, DBPath: p.DBPath,
@@ -142,31 +129,9 @@ func (p *Pool) RegisterExternal(dir, url string) {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
abs := filepath.Clean(dir) abs := filepath.Clean(dir)
s := &Server{URL: url, Host: p.Host, PollInterval: 0} p.segs[abs] = &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
if p.rootDir != "" && abs == p.rootDir { if p.rootDir != "" && abs == p.rootDir {
p.root = s p.root = p.segs[abs]
}
}
// 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
} }
} }

View File

@@ -142,25 +142,13 @@ func (w *Worker) hasPostMortemTrace(ctx context.Context, taskID int64) bool {
// //
// Статус задачи НЕ меняется (failed/timeout остаётся достигнутым); собственные // Статус задачи НЕ меняется (failed/timeout остаётся достигнутым); собственные
// сбои постмортема не влияют на исход задачи — только логируются. // сбои постмортема не влияют на исход задачи — только логируются.
// postMortem запускает постмортем-агент для задачи, завершившейся failed/timeout: func (w *Worker) postMortem(ctx context.Context, task *storage.Task) {
// собирает сессии 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) {
if w.Store == nil || w.Runner == nil { if w.Store == nil || w.Runner == nil {
return return
} }
if w.hasPostMortemTrace(ctx, task.ID) { if w.hasPostMortemTrace(ctx, task.ID) {
return return
} }
if projectDir == "" {
projectDir = w.Worktree
}
traces, err := w.Store.GetTraces(ctx, task.ID) traces, err := w.Store.GetTraces(ctx, task.ID)
if err != nil { if err != nil {
@@ -184,7 +172,7 @@ func (w *Worker) postMortem(ctx context.Context, task *storage.Task, projectDir
if tr.SessionID == "" { if tr.SessionID == "" {
continue 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 { if tErr != nil {
log.Printf("worker: task %d: постмортем: транскрипт %s (%s): %v", task.ID, tr.Agent, tr.SessionID, tErr) log.Printf("worker: task %d: постмортем: транскрипт %s (%s): %v", task.ID, tr.Agent, tr.SessionID, tErr)
continue continue
@@ -216,7 +204,7 @@ func (w *Worker) postMortem(ctx context.Context, task *storage.Task, projectDir
return 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 { if rErr != nil {
log.Printf("worker: task %d: постмортем: запуск: %v", task.ID, rErr) log.Printf("worker: task %d: постмортем: запуск: %v", task.ID, rErr)
w.finalizeTrace(ctx, traceID, storage.TraceFailed, rErr.Error()) 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: запускает постмортем, если задача // postMortemAfter — defer-хук из runTask: запускает постмортем, если задача
// завершилась failed/timeout. Собственные ошибки постмортема не мешают // завершилась failed/timeout. Собственные ошибки постмортема не мешают
// исходному результату задачи (возвращаемый *error только читается). // исходному результату задачи (возвращаемый *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 { if task.Status != storage.StatusFailed && task.Status != storage.StatusTimeout {
return return
} }
w.postMortem(ctx, task, projectDir) w.postMortem(ctx, task)
} }

View File

@@ -14,14 +14,11 @@ var devPromptTemplate = template.Must(template.New("dev").Parse(`Ты — dev-а
{{if .Title}}Название: {{.Title}}{{end}} {{if .Title}}Название: {{.Title}}{{end}}
{{if .Goal}}Цель: {{.Goal}}{{end}} {{if .Goal}}Цель: {{.Goal}}{{end}}
{{if .Repos}} {{if .Repos}}
Репозитории: Репозитории (доступны как подпапки текущего каталога):
{{- range .Repos}} {{- range .Repos}}
- {{.}} - {{.}}
{{- end}} {{- end}}
{{end}} {{end}}
{{if .Primary}}
Основной репозиторий (рабочий каталог этой сессии): {{.Primary}}
{{end}}
{{if .Why}}Зачем: {{.Why}}{{end}} {{if .Why}}Зачем: {{.Why}}{{end}}
{{if .AC}}Критерии готовности: {{if .AC}}Критерии готовности:
{{.AC}}{{end}} {{.AC}}{{end}}
@@ -33,8 +30,8 @@ var devPromptTemplate = template.Must(template.New("dev").Parse(`Ты — dev-а
{{end}} {{end}}
**Инструкции:** **Инструкции:**
1. Рабочий каталог — сам основной репозиторий ({{.Primary}}): правь файлы в нём прямо, относительными путями (./...). 1. Рабочий каталог — общий корень, в котором лежат все репозитории по именам.
{{if .Others}} Связанные репозитории задачи лежат рядом, в ../имя_репо/ (здесь cwd — основной репозиторий): меняй их согласованно, если этого требует задача.{{else}} Работа ведётся только в нём — связанных репозиториев у задачи нет.{{end}} Правь файлы внутри нужного репозитория (./имя_репо/...). Связанные репозитории меняй согласованно.
2. Напиши код, реализующий задачу. 2. Напиши код, реализующий задачу.
3. Убедись, что все acceptance criteria выполнены. 3. Убедись, что все acceptance criteria выполнены.
4. Пользуйся встроенными инструментами opencode (чтение файлов, поиск, редактирование). 4. Пользуйся встроенными инструментами opencode (чтение файлов, поиск, редактирование).
@@ -47,14 +44,9 @@ type DevPromptData struct {
Title string Title string
Goal string Goal string
Repos []string Repos []string
// Primary — основной (первый) репозиторий задачи: рабочий каталог сессии Why string
// dev — это именно его папка (сервер opencode стартует из неё). AC string
Primary string Steps []storage.Step
// Others — остальные репозитории задачи (доступны рядом как ../имя_репо/).
Others []string
Why string
AC string
Steps []storage.Step
// ReviewFeedback — замечания ревьюера при повторном прогоне dev // ReviewFeedback — замечания ревьюера при повторном прогоне dev
// (не пусто → dev должен исправить именно это). // (не пусто → dev должен исправить именно это).

View File

@@ -201,12 +201,7 @@ func (w *Worker) pollAndDispatch(ctx context.Context) error {
func (w *Worker) runTask(ctx context.Context, task *storage.Task) (err error) { func (w *Worker) runTask(ctx context.Context, task *storage.Task) (err error) {
// 0. постмортем-анализ: если задача завершилась failed/timeout — после // 0. постмортем-анализ: если задача завершилась failed/timeout — после
// выхода из runTask (всех путей) запускаем анализ сессий dev/reviewer. // выхода из runTask (всех путей) запускаем анализ сессий dev/reviewer.
// projectDir (каталог основного репозитория) объявляем заранее — defer defer w.postMortemAfter(ctx, task, &err)
// захватывает переменную, её значение подставится к моменту возврата.
var projectDir string
defer func() {
w.postMortemAfter(ctx, task, projectDir, &err)
}()
// 1. проверяем статус // 1. проверяем статус
if task.Status != storage.StatusApproved { if task.Status != storage.StatusApproved {
@@ -246,12 +241,8 @@ func (w *Worker) runTask(ctx context.Context, task *storage.Task) (err error) {
} }
} }
// 3. cwd — папка основного репозитория: сервер opencode для работы стартует // 3. cwd — общий каталог (вариант A: один dev видит все репозитории).
// из самого репозитория (подхватывает .serena, экономит токены на поиск cwd := w.Worktree
// файлов из корня worktree). Служебный root-сервер в worktree остаётся
// для аналитика; пул поднимает свой serve на каждый репозиторий работы.
cwd := w.repoDirOf(repos[0])
projectDir = cwd
// Цикл dev → review, до maxReviewIterations. // Цикл dev → review, до maxReviewIterations.
var feedback []string var feedback []string
@@ -262,8 +253,6 @@ func (w *Worker) runTask(ctx context.Context, task *storage.Task) (err error) {
Title: task.Title, Title: task.Title,
Goal: task.Goal, Goal: task.Goal,
Repos: repos, Repos: repos,
Primary: repos[0],
Others: repos[1:],
Why: task.Why, Why: task.Why,
AC: task.AC, AC: task.AC,
Steps: task.Steps, Steps: task.Steps,