agent-ecosystem/docs/iterations/iteration-04-messaging-review.md

15 KiB
Raw Permalink Blame History

Итерация 04 — Messaging + Review (Inbox + ReviewDialog)

Historical note This document captures the planned scope and assumptions at iteration time. It is not the source of truth for the current product contract. For the current review flow, see ../team-management/README.md and ../team-management/kanban-design.md.

Эта итерация добавляет панель активности (inbox messages) и отправку сообщений тиммейтам, а также закрывает MVP review-flow: Request Review → Approve / Request Changes.

Основание:

  • docs/team-management/implementation.md (Iteration 4: Messaging + Review)
  • docs/team-management/research-inbox.md (формат, race conditions, messageId verify)
  • docs/team-management/research-messaging.md (почему inbox, ограничения доставки)
  • docs/team-management/kanban-design.md (review flow, allowed transitions)
  • docs/team-management/research-tasks.md (безопасная запись status при Fix)

Цель итерации

  • Во вкладке Team появляется ActivityTimeline (история сообщений) и MessageComposer (отправка DM).
  • Реализован review-flow:
    • Request Review: перевод задачи в REVIEW + (если есть reviewer) уведомление в его inbox.
    • Approve: перевод задачи в APPROVED.
    • Request Changes: опциональный комментарий → задача возвращается исполнителю:
      • kanban-state запись удаляется (задача вернётся в IN PROGRESS по task.status)
      • task.status становится in_progress (atomic write + verify)
      • отправляется сообщение ownerу с описанием правок
  • Live refresh: изменения inbox/task/kanban отражаются в UI (уже существующий team:change + coalesce).

Не-цели (строго вне scope)

  • Hard interrupt (прерывание mid-turn) — ограничение платформы, Phase 2.
  • Архивация inbox / очистка истории / JSONL формат — Phase 2.
  • Полноценный drag-and-drop для review — Phase 2 (пока click-to-move/кнопки).
  • Round-robin балансировка reviewerов и reviewHistory — Phase 2.
  • Любые write-path, связанные с созданием задач и .highwatermark.

Контракт итерации (Main → Preload → Renderer)

Shared types (src/shared/types/team.ts)

Добавляем/расширяем:

  • InboxMessage (как в research-inbox.md):
    • from: string
    • text: string (plain text или JSON-строка)
    • timestamp: string (ISO)
    • read: boolean
    • summary?: string
    • color?: string
    • messageId?: string (в наших исходящих обязателен)
  • SendMessageRequest:
    • member: string
    • text: string
    • summary?: string
    • from?: 'user' | string (по умолчанию 'user')
  • SendMessageResult:
    • deliveredToInbox: boolean (означает “записано в файл и verify прошёл”)
    • messageId: string

Расширяем TeamData:

  • messages: InboxMessage[] (агрегированные по всем inbox, отсортированные по timestamp)

Расширяем ResolvedTeamMember:

  • status: 'active' | 'idle' | 'terminated' | 'unknown'
  • lastActiveAt: string | null
  • messageCount: number
  • color?: string

Правила статуса (из docs/team-management/README.md):

  • ACTIVE: last activity < 5 минут
  • IDLE: last activity ≥ 5 минут
  • TERMINATED: получен shutdown-event с approve=true (в доках встречаются разные названия, поэтому считаем terminated если structured text после JSON.parse имеет:
    • type === 'shutdown_response' && approve === true, или
    • type === 'shutdown_approved', или
    • type === 'shutdown_response' && approved === true
  • если данных нет → unknown

IPC каналы

В src/preload/constants/ipcChannels.ts добавляем:

  • TEAM_SEND_MESSAGE = 'team:sendMessage'
  • TEAM_REQUEST_REVIEW = 'team:requestReview'
  • TEAM_UPDATE_KANBAN уже существует с итерации 03 и расширяется (см. ниже) для request_changes.

Важно: в v7-плане перечислены именно team:sendMessage, team:requestReview, team:updateKanban. В этой итерации не вводим новые каналы сверх этого набора.

TeamsAPI (src/shared/types/api.ts)

Расширяем:

  • sendMessage(teamName: string, req: SendMessageRequest): Promise<SendMessageResult>
  • requestReview(teamName: string, taskId: string): Promise<void>
  • updateKanban(teamName: string, taskId: string, patch: UpdateKanbanPatch): Promise<void> (из итерации 03, расширяем patch)

UpdateKanbanPatch — расширение в итерации 04 (без двусмысленностей)

К существующим операциям добавляем:

  • { op: 'request_changes'; comment?: string }

Семантика request_changes (всё внутри main, как единая операция):

  1. Удалить kanban-state запись для taskId (remove)
  2. Обновить task файл: status = 'in_progress' (atomic write + verify)
  3. Отправить сообщение ownerу задачи с комментарием (если owner отсутствует → ошибка)

Definition of Done (DoD)

  • Messaging UI
    • В Team tab есть ActivityTimeline (список сообщений) и MessageComposer.
    • Можно выбрать участника и отправить сообщение → появляется “Delivered” (или ошибка).
    • Сообщение записывается в ~/.claude/teams/{team}/inboxes/{member}.json с messageId.
  • Review UI
    • На задачах в DONE есть действие “Request Review”.
    • В REVIEW есть кнопки “Approve” и “Request Changes”.
    • “Request Changes” открывает диалог с опциональным комментарием.
  • Backend safety
    • Inbox write: atomic write + verify messageId + retry/backoff (по плану ниже).
    • Task status write (Fix): atomic write + verify status + warning в UI при конфликте.
  • Качество
    • pnpm typecheck проходит.
    • pnpm test проходит.

Выходные изменения (файлы)

Новые файлы

  • src/main/services/team/TeamInboxWriter.ts (write-path: atomic + verify + retry + lock)
  • src/main/services/team/TeamTaskWriter.ts (минимум: updateStatus(teamName, taskId, status)), чтобы не смешивать чтение и запись
  • src/renderer/components/team/activity/ActivityTimeline.tsx
  • src/renderer/components/team/activity/ActivityItem.tsx
  • src/renderer/components/team/activity/MessageComposer.tsx
  • src/renderer/components/team/dialogs/ReviewDialog.tsx

Изменяемые файлы

  • src/shared/types/team.ts (InboxMessage, SendMessage*, member status, TeamData.messages)
  • src/shared/types/api.ts (TeamsAPI methods)
  • src/preload/constants/ipcChannels.ts (+ 3 канала)
  • src/preload/index.ts (bridge для sendMessage/requestReview/updateKanban)
  • src/main/ipc/guards.ts (+ validateMemberName(), validateFromField() если потребуется)
  • src/main/ipc/teams.ts (+ handlers)
  • src/main/services/team/TeamInboxReader.ts (расширить: getMessagesFor(), getMessages())
  • src/main/services/team/TeamMemberResolver.ts (status/messageCount/color/lastActiveAt + terminated detection)
  • src/main/services/team/TeamDataService.ts (подтянуть messages + review methods; опираться на kanban из итерации 03; в requestReview — sendMessage первому reviewer при наличии)
  • src/main/services/team/index.ts (exports)
  • src/renderer/store/slices/teamSlice.ts (actions + sending states)
  • src/renderer/components/team/TeamDetailView.tsx (layout: members + kanban + activity)
  • src/main/services/infrastructure/FileWatcher.ts (ничего нового: inbox/task/kanban уже должны попадать в team-change)

Порядок работ (runbook) с контрольными точками

CP0 — типы компилируются

  1. Обновить src/shared/types/team.ts и src/shared/types/api.ts (контракты выше).
  2. pnpm typecheck

CP1 — Inbox read/write в main готов (без UI)

  1. Inbox read

    • listInboxNames(teamName) (уже есть/или оставить) — список *.json без расширения
    • getMessagesFor(teamName, member):
      • ENOENT → []
      • invalid JSON → [] + warning
    • getMessages(teamName):
      • читает все inbox *.json (параллельно, best-effort)
      • merge в один массив
      • сортировка по timestamp (desc)
  2. Inbox write: atomic + verify + retry

Реализация должна следовать research-inbox.md:

  • Каждое сообщение, которое пишет приложение, включает:
    • messageId = crypto.randomUUID()
    • from = 'user' (по умолчанию)
    • read = false
    • timestamp = new Date().toISOString()

Алгоритм (в main):

read inbox JSON array (если нет → [])
append message
atomicWriteAsync(tmp + rename)
read back
verify messageId exists
если не найден → retry (до 3) с backoff 10/20/40ms

Параллелизм:

  • внутри процесса сериализуем записи в один inboxPath через withInboxLock(inboxPath, fn), чтобы два IPC вызова не затёрли друг друга.

Fallback по from:

  • делаем это не магией, а контрактом API:
    • SendMessageRequest.from опционален, но если задан — валидируем (validateFromField или reuse validateMemberName)
    • UI в этой итерации не обязано показывать выбор from; по умолчанию отправляем from: 'user'
    • для отладки можно вызывать через DevTools sendMessage(..., { from:'debug-user', ... })
  1. pnpm test (если добавлялись новые unit-тесты) и pnpm typecheck

CP2 — Review actions в main + IPC

  1. Task status write (Fix)

Новый TeamTaskWriter.updateStatus(teamName, taskId, status):

  • читает ~/.claude/tasks/{teamName}/{taskId}.json
  • меняет только status
  • пишет через atomicWriteAsync
  • перечитывает и проверяет, что status равен нужному
  • при конфликте (агент перезаписал) → throw, чтобы UI показал warning
  1. TeamDataService
  • getTeamData(teamName) теперь возвращает также messages
  • requestReview(teamName, taskId):
    • updateKanban(taskId, { op:'set_column', column:'review' }) (reviewStatus: pending)
    • выбрать reviewer:
      • если kanbanState.reviewers[] не пуст → первый элемент (round-robin — Phase 2)
      • иначе reviewer отсутствует (manual review)
    • если reviewer выбран → sendMessage(reviewer, "...")
  • approve делаем напрямую через updateKanban(taskId, { op:'set_column', column:'approved' })
  • request_changes делаем через updateKanban(taskId, { op:'request_changes', comment }) (см. семантику patch выше)
  1. IPC
  • TEAM_SEND_MESSAGE: принимает teamName, SendMessageRequest
  • TEAM_REQUEST_REVIEW: принимает teamName, taskId
  • TEAM_UPDATE_KANBAN: принимает teamName, taskId, UpdateKanbanPatch (расширенный)

Guards:

  • validateMemberName() обязателен (защита от path traversal, member используется в пути inbox файла).
  • validateTaskId() уже есть с итерации 03.
  1. pnpm typecheck

CP3 — UI: ActivityTimeline + MessageComposer + ReviewDialog

  1. Layout
  • Team tab становится 3-панельным:
    • слева Members
    • центр Kanban
    • справа Activity (messages + composer)

Чтобы не потерять доступ к “сырому списку задач”, фиксируем простой UI-компромисс:

  • правый panel становится табами: Activity и Tasks
  • по умолчанию открываем Activity, но Tasks остаётся доступным (и переиспользует текущий TaskList)
  1. ActivityTimeline
  • Рендерит TeamData.messages (последние N, например 200) с:
    • цветной dot (если есть color)
    • from, summary (или короткий preview text)
    • timestamp (relative time)
  1. MessageComposer
  • select получателя (из members)
  • textarea (минимум 12 строки)
  • send → вызывает store action → IPC sendMessage
  • состояния:
    • sending / delivered / error
  1. ReviewDialog
  • открывается при “Request Changes”
  • comment optional
  • submit → updateKanban(..., { op:'request_changes', comment })
  1. Ручная проверка
  • Отправить сообщение → файл inbox обновился, message появился в ActivityTimeline после refresh.
  • Нажать Request Review → задача в REVIEW, при наличии reviewerа в kanban-state.reviewers ему ушло сообщение.
  • Approve → задача в APPROVED.
  • Request Changes → задача ушла исполнителю (status=in_progress) + отправлено сообщение ownerу.
  1. pnpm test

Риски и митигации

  • Race condition inbox: атомарная запись не решает overwrite race, поэтому делаем messageId verify + retry/backoff, плюс in-process withInboxLock.
  • Конфликт при записи task.status: после write делаем verify; если agent перезаписал — показываем warning в UI, не делаем silent fail.
  • Большие inbox: ограничиваем количество отображаемых сообщений (например 200) и добавляем “Show more” позже (итерация 05).