opencode-config/docs/decisions/019-pr-46-review-posting-tools.md
Sergey 1acac5229f
feat(tools): deterministic review posting tools (post-review, post-docs-review) (#46)
* feat(tools): add post-review and post-docs-review tools

* refactor(agents): use post-review/post-docs-review tools in reviewer and docs-reviewer

* feat(permissions): add post_review and post_docs_review to agent.tools map

* test(tools): add tests for post-review and post-docs-review tools

* docs(handoff): add handoff ADR and project-map for review posting tools

* docs(handoff): set PR number

* docs: fix PR#45→PR#46 refs in project map

---------

Co-authored-by: opencode-agent <agent@slaid098.dev>
2026-07-24 18:26:48 +03:00

6.9 KiB
Raw Blame History

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

Статус

Accepted (2026-07-24)

Контекст

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 SummaryREVIEW_VERDICT_RE не матчит → pipeline NOT_DONE (блокировка)
  2. Verdict typoAPROVE вместо APPROVE, REQUEST_CHANGES с пробелом → regex не матчит verdict → NOT_DONE
  3. No mutual exclusion — любой агент может постить ## Code Review Summary (нет guard "только reviewer")
  4. False positiveDOCS_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 commentpost_review({ pr_number, verdict, body }). 3 сценария (APPROVE/REQUEST_CHANGES/NEEDS_DISCUSSION). Rules 7/8 обновлены.
  • docs-reviewer.md — секция "PR Comment (mandatory)" переписана: gh pr commentpost_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.