# ADR-019: Deterministic review posting tools (post-review, post-docs-review)

## Статус
Accepted (2026-07-24)

## Superseding note (PR#60, 2026-07-25)

Часть решений ADR-019 пересмотрена в PR#60 (`fix(tools): drop hardcoded --repo in post-review/post-docs-review`):

1. **Хардкод `--repo slaid098/opencode-config` убран** из `post-review.ts:16` и `post-docs-review.ts:16`. Теперь `gh pr comment N --body <comment>` без `--repo` — gh auto-detect'ит репо из `context.worktree` (cwd), симметрично с `create-pr.ts`/`merge-pr.ts`/`commit.ts`. Tools больше не привязаны к `slaid098/opencode-config` и работают из любого репо с `origin` remote.

2. **Rejected alternative "Параметризовать `--repo` через `git remote`" пересмотрена.** В ADR-019 эта альтернатива была отклонена как "out of scope. Сейчас tools специфичны для slaid098/opencode-config". После того как tools установили глобально (применимы ко всем репо), хардкод стал багом: comment постился в `slaid098/opencode-config` независимо от cwd → silent misroute (PR#29 cross-post incident: 5 комментов предназначавшихся `opencode-voice-dictation` PR#29 ушли в `opencode-config` PR#29). Достаточно убрать хардкод — параметризация через `git remote get-url` не нужна, gh auto-detect справляется.

3. **Расхождение skill-шаблонов ↔ промптов устранено.** `run-pipeline/SKILL.md` Template B (docs-review) и Template C (code_review) предписывали raw `gh pr comment M --body "## ... Summary\n..."`. Промпты `reviewer.md`/`docs-reviewer.md` предписывали `post_review`/`post_docs_review` tool. Оба пути технически разрешены (`gh pr comment*` в allow-list), но расхождение — code smell, модель могла выбрать любой. Template B/C обновлены использовать tool (один canonical путь, формат гарантирован zod-enum).

4. **Добавлена инструкция обработки сбоя tool** в `reviewer.md` и `docs-reviewer.md`: если tool вернул `⚠️ ...failed` — СООБЩИ оркестратору и STOP, не fallback на raw `gh pr comment`. Раньше такой инструкции не было (см. memory `post-review-post-docs-review-tool-vs-skill-discrepancy.md` — "Fallback instructions — НЕТ"), что позволяло агенту игнорировать сбой (вероятная причина PR#29 silent failure).

5. **Из не-git директории tool'зы падают с явной ошибкой** (gh exit non-zero → `⚠️ ...failed`), а не тихим misroute'ом. Условие работы: `context.worktree` указывает на git-репо с `origin` remote.

## Контекст

Reviewer и docs-reviewer агенты постят verdict-комментарии на PR через raw `gh pr comment` с ручным форматированием heading + verdict line:

- reviewer: `## Code Review Summary` + `### Verdict: APPROVE|REQUEST_CHANGES|NEEDS_DISCUSSION`
- docs-reviewer: `## Docs Review Summary` + `### Verdict: APPROVE|FIXED|NO_CHANGES`

`pipeline-status.py` парсит эти комментарии regex'ами для определения статуса фаз REVIEW и DOCS:
- `REVIEW_VERDICT_RE = re.compile(r"## Code Review Summary.*?###\s*Verdict:\s*(\w+)", re.IGNORECASE | re.DOTALL)`
- `DOCS_REVIEW_RE = re.compile(r"Docs Review", re.IGNORECASE)` — substring match

Риски текущего подхода (raw `gh pr comment`):
1. **Wrong heading** — reviewer случайно пишет `## Code Review` (без "Summary") или `## Review Summary` → `REVIEW_VERDICT_RE` не матчит → pipeline NOT_DONE (блокировка)
2. **Verdict typo** — `APROVE` вместо `APPROVE`, `REQUEST_CHANGES` с пробелом → regex не матчит verdict → NOT_DONE
3. **No mutual exclusion** — любой агент может постить `## Code Review Summary` (нет guard "только reviewer")
4. **False positive** — `DOCS_REVIEW_RE` substring match `Docs Review` (без `## ` prefix требования), теоретический false positive если любой комментарий упомянет "Docs Review" в тексте

Root cause: формат комментария контролируется промптом (soft guard), не type system. Модель может опечататься, забыть heading, изменить формат.

## Решение

Детерминированные TS tools, которые гарантируют heading + verdict формат через type system:

### 1. `post-review.ts` tool (`.opencode/tools/post-review.ts`)

- Args: `pr_number: number`, `verdict: enum(APPROVE, REQUEST_CHANGES, NEEDS_DISCUSSION)`, `body: string`
- Генерирует comment: `## Code Review Summary\n\n${body}\n\n### Verdict: ${verdict}`
- Вызывает `gh pr comment N --body <comment> --repo slaid098/opencode-config` через spawnSync
- Body — содержимое БЕЗ heading и verdict (tool добавляет их сам)
- Verdict enum (zod) гарантирует: только APPROVE/REQUEST_CHANGES/NEEDS_DISCUSSION, опечатки отклоняются на schema layer

### 2. `post-docs-review.ts` tool (`.opencode/tools/post-docs-review.ts`)

- Args: `pr_number: number`, `verdict: enum(APPROVE, FIXED, NO_CHANGES)`, `body: string`
- Генерирует comment: `## Docs Review Summary\n\n${body}\n\n### Verdict: ${verdict}`
- Вызывает `gh pr comment N --body <comment> --repo slaid098/opencode-config` через spawnSync

### 3. Agent integration

- `reviewer.md` — секция "Output Format" переписана: `gh pr comment` → `post_review({ pr_number, verdict, body })`. 3 сценария (APPROVE/REQUEST_CHANGES/NEEDS_DISCUSSION). Rules 7/8 обновлены.
- `docs-reviewer.md` — секция "PR Comment (mandatory)" переписана: `gh pr comment` → `post_docs_review({ pr_number, verdict, body })`.

### 4. Role-based access (agent.tools map)

`.opencode/opencode.json` `agent.tools` map расширен:

| Agent | post_review | post_docs_review |
|---|---|---|
| general | false | false |
| reviewer | true | false |
| docs-reviewer | false | true |
| memory-syncer | false | false |

Mutual exclusion: только reviewer может постить code review, только docs-reviewer — docs review. General и memory-syncer не могут постить reviews. Это устраняет риск "любой агент постит review comment".

### 5. pipeline-status.py совместимость

Regex'ы НЕ изменены. Tools генерируют ровно тот формат, который regex ожидает:
- `## Code Review Summary` (exact heading) + `### Verdict: <verdict>` → `REVIEW_VERDICT_RE` матчит
- `## Docs Review Summary` (contains `Docs Review`) → `DOCS_REVIEW_RE` матчит

### 6. Тестовая инфраструктура

`tests/_ts_loader.mjs` расширен:
- zodShim: добавлен `enum: () => chain()` метод (для `tool.schema.enum(VERDICTS)`)
- `stripTs`: strip `as const` assertions + strip `type <Name> = ...` type alias declarations (TS-only синтаксис новых tools)

24 новых Python теста (12 на tool) + 14 TS тестов (документационные).

## Альтернативы

- **Оставить raw `gh pr comment` + усилить промпт** — отклонено: промпт = soft guard, модель (GLM) игнорирует правила форматирования. Root cause (формат контролируется промптом, не type system) не устранён. Документировано в PR#85 (docs-reviewer marker) — промпт-правила ненадёжны.

- **Validate comment format post-hoc в pipeline-status.py** — отклонено: pipeline-status.py уже парсит regex'ами, но это detection, не prevention. Comment уже постит с wrong heading → pipeline NOT_DONE → нужно перезапустить reviewer. Tool предотвращает ошибку на этапе создания (prevention > detection).

- **Параметризовать `--repo` через `git remote` (как ADR-007 для pipeline-status.py)** — отклонено: out of scope. `merge-pr.ts` тоже хардкодит `--squash --delete-branch` (не параметризует repo). Для других репо — отдельный PR (параметризация всех tools). Сейчас tools специфичны для `slaid098/opencode-config`.

- **Запретить raw `gh pr comment` в allow-list (принудить к tool)** — отклонено: оставлено для обратной совместимости (fallback). Tools используют spawnSync напрямую (не через bash permission layer), так что raw `gh pr comment*` в allow-list не конфликтует. Если захотеть strict tool-only — отдельный PR с deny правилом `gh pr comment*`.

- **Расширить `DOCS_REVIEW_RE` до `## Docs Review Summary` (exact heading)** — отклонено: risk of breaking existing comments. Tools уже генерируют exact heading, но старые comments (до этого PR) могут иметь другой формат. Substring match остаётся как tolerant fallback. Future PR может tighten regex после migration.