opencode-config/docs/decisions/019-pr-46-review-posting-tools.md
Sergey f96acaaa75
fix(tools): drop hardcoded --repo in post-review/post-docs-review (#61)
* fix(tools): drop hardcoded --repo in post-review/post-docs-review

* fix(skills): sync run-pipeline templates to use review tools

* fix(agents): add tool failure handling to reviewer/docs-reviewer

* docs(adr): supersede ADR-019 with hardcoded repo removal note

* docs(handoff): scaffold handoff and ADR for PR

* docs(handoff): set PR number

* fix(ci): reformat post-review/post-docs-review test asserts for ruff format

---------

Co-authored-by: opencode-agent <agent@opencode.local>
2026-07-25 03:55:50 +03:00

9.8 KiB
Raw Permalink Blame History

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 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.