postmortem spec multi-agent

Style-Write Unification:
двухагентная коллаборация

Анализ работы Claude + Codex над спекой инспектор-стилей. Что получилось, что сломалось, как починить.
📅 2026-04-14 → 2026-04-16 👤 Alex Ultra (observer) 🤖 Claude Sonnet 4.6 🤖 Codex GPT-5.4 📝 5 spec файлов
Содержание
  1. TL;DR — что произошло
  2. Таймлайн коллаборации
  3. Архитектура спеки (что наработали)
  4. Канонические решения
  5. Postmortem: почему коллаборация развалилась
  6. Протокол v2 — конкретный дизайн
  7. Что осталось открытым
  8. Action items

1. TL;DR — что произошло

5
spec файлов
4436
строк plan.md
3
P1 финдинга закрыто
2
P1 открыто
~24ч
общая длительность
0
тестов написано

Что хотели

Унифицировать запись стилей инспектора между VS Code-расширением и SaaS. Сделать стратегию шеренной, framework-aware, source-aware, condition-aware и theme-aware. Два агента (Claude и Codex) должны были автономно дискутировать на таймерах и сходиться к рабочей спеке.

Что вышло

Спека получилась богатая (5 документов, ~10к строк), архитектурно цельная — есть StyleWritePlan union, framework adapter umbrella, source ownership с confidence, theme resolver, prop mappers. Codex нашёл 3 P1 регрессии в моём прошлом пасе — две из них я починил (RuntimeThemeContext проза, ScriptObjectStylePlan + mantine), одну починил частично (CssFilePlan invariants — оставил optional с runtime guard вместо discriminated union, и Codex это снова поймал в следующем ревью).

Что сломалось в коллаборации

Дискуссии не было. Был asymmetric ping-pong: Claude правит → Codex поллит и ревьюит → Claude получает явный пинг от Alex и снова правит. Codex запущен как daemon с 10-минутным polling-loop; Claude каждый раз — fresh session, инициируется только Alex'ом вручную. Никакого механизма «Codex разбудил Claude» нет. Convergence criterion отсутствует. Споров между агентами не было — каждый просто принимает фиксы другого как должное.

Главная поломка
Codex может опросить ledger в любой момент, Claude — нет. Alex был необходим как живой message bus между двумя агентами. Это не «два агента работают вместе», это «два агента работают через человека».

2. Таймлайн коллаборации

Claude
Codex
Observer (Alex)
Поломка протокола

День 1 — 2026-04-14 (вечер): elaboration в одиночку

14:00 CEST
Observer: запустил параллельно Claude и Codex на одну задачу — расширить style-write-unification план.
Каждый агент работал отдельно, не зная о существовании другого. Claude дописывал главный plan.md, Codex одновременно правил его же.
14:00–00:00 CEST
⚠ Невидимая параллельная работа
Никакого ledger ещё нет. Оба агента редактируют один и тот же файл. Конфликты разрешаются git'ом «последний победил». Часть decisions одного агента переписывается другим без уведомления.

День 2 — 2026-04-15 (утро): попытка установить протокол

12:40 CEST
Observer → Codex: «напиши все что сегодня сделали в workprocess.md».
Решено создать ledger-файл для координации. Codex пишет первую версию.
12:42 CEST
Observer → Codex: «сделай для него хорошую структуру».
13:05 CEST
Observer → Codex: «второй агент описал проделанную работу — ты тоже подпишись и сделай ревью».
Первое явное упоминание «второго агента». Codex обнаруживает Claude через ledger.
13:21 CEST
Observer → Codex: формализует протокол. Правила: статус, ожидание, разделение ролей (Claude — elaboration, Codex — review).
13:28 CEST
Codex review: 3 P1/P2 находки.
P1: RuntimeThemeContext противоречие в прозе. P1: CssFilePlan потерял required-field invariants после union refactor. P2: ScriptObjectStylePlan исключил Mantine.

День 2 — 2026-04-15 (день): polling loop без партнёра

13:33 CEST
Codex: новая сессия, 10-минутный repeat-cadence на чтение ledger. Ждёт ответа Claude.
13:33–14:30 CEST
⚠ Тишина в эфире
Codex поллит каждые 10 минут — нет новых записей. Claude никем не запущен. Без observer'а коллаборация мертва.
14:30 CEST
Observer → Claude: «разбери ревью» (явный пинг).
14:30–14:58 CEST
Claude analysis + fixes
Прочитал ledger, разобрал 3 находки Codex. Применил фиксы к plan.md. Записал 📍 Review Session 14:58 CEST с описанием изменений.
15:09 CEST
Codex review #2: 2 новых P1 находки.
P1: PlainCssFilePlan всё ещё compile-valid-invalid (cascadeOwner? и createMode? оба optional с runtime guard). P1: Mixed Tailwind + CSS Modules tie-breaker всё ещё не определён.
15:09–18:00 CEST
⚠ Открытые findings висят
Codex логирует находки, ставит обоих агентов в done, идёт спать. Claude не оповещён, не приходит фиксить. Convergence не достигнут — но никто этого не замечает.

День 3 — 2026-04-16: реконструкция и postmortem

~01:00 CEST
Observer → Claude: «У меня депрессия. Я окончательно запутался... сделай полный отчёт».
Этот документ — результат.

3. Архитектура спеки (что наработали)

Спека описывает шеренный pipeline для записи стилей: один путь для VS Code-расширения и SaaS. Три ключевых уровня — capabilities (что доступно в проекте/элементе), source ownership (кто владеет свойством сейчас), write plan (конкретная мутация).

3.1 Pipeline записи (write flow)

Inspector control user edits one property InspectorValueCodec validate + normalize to inspector form only StyleWriteContext projectCapabilities, elementFacts, condition, runtimeThemeContext StyleWriteManager.createPlan(ctx) routes through framework adapters, mappers, source resolver, AI router TailwindPlan elementClass tailwind-v3 / v4 static / dynamic CssFilePlan cssStyleRule CssModulesFilePlan | PlainCssFilePlan ScriptObjectStylePlan scriptReactStyleRule inline / emotion / sx / v-extract / mantine ScriptTemplateStylePlan scriptNativeStyleRule styled.div`` css`...` AdapterPropPlan adapterKnownProp tamagui / chakra / mui-sx / mantine ArbitraryPropPlan arbitraryElementProp no CSS semantics requestedStyles: {} Adapter writer (per-target value conv.) → file mutation + FastPatch tag
Manager диспатчит на один из 6 планов через развилку по sourceForm: elementClass → TailwindPlan, cssStyleRule → CssFilePlan, scriptReactStyleRule → ScriptObjectStylePlan, scriptNativeStyleRule → ScriptTemplateStylePlan, adapterKnownElementProp → AdapterPropPlan, arbitraryElementProp → ArbitraryPropPlan. Per-target value conversion (opacity 50→0.5 и т.п.) живёт в адаптерных writer'ах, не в шеренном codec'е.
Spec correction (F-010)
В первой версии спеки InspectorValueCodec отвечал за target-conversion: opacity 50 → 0.5 (CSS), 50 → '50' (Tailwind), 50 → 0.5 (Tamagui). Это неверно. Codec должен только валидировать и нормализовывать значение к inspector-form'е (например: '50%' / '50' / 50 → '50'). Конверсия в target value — забота адаптера, потому что только адаптер знает value space своего target'а (CSS-opacity 0..1, Tailwind utility scale 0..100, Tamagui prop number 0..1, ...). Один центральный target-converter — это leak адаптерных знаний в шеренный layer. См. F-010 в open items.

3.2 Source Tab routing — кто решает куда писать

какая Source Tab выбрана? Explicit user tab «CSS Module» / «inline» → deterministic resolver confidence: exact Computed tab + CSS Modules / plain CSS → AI StyleSourceRouter route: ai-assisted Tailwind owner single class system → static or dynamic plan confidence: exact No safe target ambiguous / no resolver → Inline fallback confidence: fallback plan.confidence: exact | probable | fallback AI-assisted роутинг → plan 'probable' (не верифицируется детерминистически)
Routing решает куда применить write. Два confidence-поля: routeDecision.confidence (метод) и plan.confidence (outcome) — ортогональны, не сливать.

3.3 Theme resolution — что особенного

Theme — отдельная подсистема. Differentiates:

ПонятиеЧто этоГде живёт
RuntimeThemeContextтекущее предпочтение пользователя light/dark/systemper-request на StyleWriteContext
theme conditionусловие в source: .dark &, data-theme='dark'StyleCondition.theme[]
theme value ownerгде определён цветовой токенcascadeOwner на PlainCssFilePlan
write targetчто мутировать: variable, branch, tokentarget.declarations

Ключевое правило: system — это IDE preference, не source condition. Resolver приводит system → light или dark до роутинга.

4. Канонические решения

РешениеСуть
CSS Modules ≠ TailwindНе аппендить Tailwind классы к CSS Modules-only элементам. Либо CSS Modules write, либо inline fallback.
Inline fallback permanentНе временный phase guard, а универсальный fallback когда никакой adapter не может безопасно построить source-write.
FastPatch ≠ source truthТолько optimistic preview. Никогда не подменяет source-write. Тесты ждут HMR/readback подтверждения.
Adapter umbrellaОдин FrameworkStyleAdapter на framework с фасетами reader/writer/sourceResolver/tokenResolver/themeResolver. Не один плоский adapter.
UI kit ≠ CSS systemshadcn/daisyUI — UI kits на Tailwind. Mantine/MUI/Chakra — могут быть и CSS system, и UI kit.
StyleCondition extensiblestate, viewport, container, media/supports, theme, selector, raw. Last preserves adapter-specific reads.
CascadeContext separate@layer, @scope, at-rule placement — это source/cascade context, не condition row.
One control = one planОдин inspector control = одно property = один StyleWritePlan. Без AI-batching через адаптеры.
InspectorValueCodec scopeПоправлено (F-010): codec только валидирует и нормализует к inspector-form'е ('50%' → '50'). Per-target conversion ('50' → 0.5 для CSS) живёт в адаптерах — у каждого target'а своё value space.

5. Postmortem: почему коллаборация развалилась

Протокол был задокументирован в workprocess.md. На бумаге выглядел разумно. На практике развалился по 6 конкретным причинам.

5.1 Asymmetric agents

Поломка #1 — несимметричный контроль
Codex запущен как long-running daemon с repeat-cadence (читает ledger каждые 10 минут). Claude — fresh session, инициируется только Alex'ом. Когда Codex добавляет findings — Claude об этом не узнаёт автоматически.

5.2 No invocation channel

Поломка #2 — нет канала пинга
Codex физически не может «разбудить» Claude. Нет API, нет webhook'а, нет shared queue. Единственный способ перенести state из Codex'а в Claude — Alex вручную пишет в Claude «прочитай ledger».

5.3 Session context loss

Поломка #3 — потеря контекста между сессиями
Каждый раз когда Alex запускает Claude заново — это новая сессия, нулевой контекст. Claude должен с нуля прочитать workprocess (~1100 строк), plan.md (4400 строк), 3 sub-spec файла. Это 30k+ токенов на one-shot чтение, и часть деталей теряется.

5.4 No convergence criterion

Поломка #4 — непонятно когда коллаборация закончена
Status board: Claude `done`, Codex `done`. Но Codex добавил 2 P1 находки и пошёл спать. По формальному протоколу — work done. По факту — есть открытые блокеры. Никакого правила «не помечать done пока есть unresolved P1 findings» нет.

5.5 No discussion / disagreement protocol

Поломка #5 — нет настоящего диалога
Каждый агент принимает фиксы другого как должное. Если Claude поверхностно починил CssFilePlan (runtime guard вместо compile-time discriminated union) — Codex это заметил и снова открыл, но не написал «я не согласен с этим подходом, я бы сделал X». Просто log new finding, дальше ждать. Convergence через тишину, а не через консенсус.

5.6 Observer как message bus

Поломка #6 — Alex носит сообщения
По итогу Alex был необходимым звеном для каждого хода. «Codex что-то нашёл — расскажи Claude». «Claude починил — расскажи Codex». Это не двухагентная коллаборация. Это observer-driven sequential workflow с двумя исполнителями.

5.7 Что в этом всё-таки сработало

Что не сломалось
  • Ledger как single source of truth — workprocess.md действительно даёт обоим агентам общий контекст. Без него было бы хуже.
  • 📍 Review Session формат — позволяет легко найти где чей turn, что сделано, какие validation команды запускались.
  • Asymmetric roles (Claude — elaboration, Codex — review) — хорошая идея в принципе. Codex пойман на adversarial review, Claude — на детализации. Каждый из них находит то, что другой пропускает.
  • Версия плана v1 → v2 — несмотря на провалы протокола, спека реально стала чище: 3 регрессии нашли и (частично) починили. Без второго агента эти баги ушли бы в production-код.

6. Протокол v2 — конкретный дизайн

Цель: исключить Alex'а как message bus. Сохранить asymmetric roles. Добавить convergence criterion и disagreement handling.

6.1 Архитектура: orchestrator + структурированный ledger

Orchestrator (cron / daemon) читает findings.yaml, решает чей turn, вызывает claude -p / codex exec Structured ledger workprocess.md (prose) + findings.yaml (machine-readable) id, owner, status, severity, raised_by Claude (per-invocation) role: elaboration, fixes claude -p "address findings owned by claude in findings.yaml" Codex (per-invocation) role: adversarial review codex exec "review last claude turn, add new findings to ledger" Observer ping (Telegram) только при disputes или после convergence: «готово, см. report.html» read & decide write turn write turn
Orchestrator — единственный «живой» процесс. Он читает структурированный ledger, решает чей turn, вызывает Claude или Codex headless. Observer пингуется только при disputes или convergence.

6.2 Структурированный ledger

Дополнить workprocess.md машинным findings.yaml рядом:

findings:
  - id: F-001
    title: "RuntimeThemeContext composition-root prose contradiction"
    severity: P1
    status: closed
    raised_by: codex
    raised_at: 2026-04-15T13:28+02:00
    fix_owner: claude
    fixed_at: 2026-04-15T14:58+02:00
    verified_by: codex
    verified_at: 2026-04-15T15:09+02:00

  - id: F-004
    title: "PlainCssFilePlan compile-valid invalid targets"
    severity: P1
    status: open
    raised_by: codex
    raised_at: 2026-04-15T15:09+02:00
    fix_owner: claude
    notes: |
      Discriminated union required (mode: 'existing-owner' | 'create-rule')
      Runtime guard insufficient.

  - id: F-005
    title: "Tailwind + CSS Modules tie-breaker policy"
    severity: P1
    status: open_question
    raised_by: codex
    raised_at: 2026-04-15T13:28+02:00
    fix_owner: observer
    notes: "Requires product decision before implementation."

6.3 Orchestrator pseudocode

#!/usr/bin/env bash
# agent-loop.sh — runs both agents until convergence
set -euo pipefail

LEDGER=docs/specs/2026-04-14-style-write-unification-workprocess.md
FINDINGS=docs/specs/2026-04-14-style-write-unification-findings.yaml
QUIET_CYCLES=0
MAX_QUIET=3

while true; do
  open_for_claude=$(yq '[.findings[] | select(.status=="open" and .fix_owner=="claude")] | length' "$FINDINGS")
  open_for_codex=$(yq '[.findings[] | select(.status=="open" and .fix_owner=="codex")] | length' "$FINDINGS")
  disputes=$(yq '[.findings[] | select(.status=="dispute")] | length' "$FINDINGS")

  if [[ $disputes -gt 0 ]]; then
    notify_observer "disputes need adjudication: $disputes"
    break
  fi

  prev_count=$(wc -l < "$LEDGER")

  if [[ $open_for_claude -gt 0 ]]; then
    claude -p "Read $LEDGER and $FINDINGS. Address all findings where fix_owner=claude AND status=open. Mark each closed in findings.yaml. Append a 📍 Review Session turn to $LEDGER."
  elif [[ $open_for_codex -gt 0 ]]; then
    codex exec "Same instruction, fix_owner=codex."
  else
    # Both sides clear → run codex review of last claude turn
    codex exec "Review the most recent 📍 turn by claude in $LEDGER. If you find new findings, append them to $FINDINGS with raised_by=codex."
  fi

  new_count=$(wc -l < "$LEDGER")
  if [[ $new_count -eq $prev_count ]]; then
    QUIET_CYCLES=$((QUIET_CYCLES + 1))
    [[ $QUIET_CYCLES -ge $MAX_QUIET ]] && { notify_observer "convergence reached"; break; }
  else
    QUIET_CYCLES=0
  fi

  sleep 60
done

6.4 Что новый протокол даёт

Поломка v1Решение в v2
Asymmetric agentsСимметрично: оба вызываются orchestrator'ом одинаково
No invocation channelOrchestrator сам вызывает claude -p / codex exec
Session context lossКаждое invocation короткое и focused (только open findings owned by you)
No convergence criterion3 quiet cycles подряд = converged
No disagreement protocolStatus dispute блокирует convergence, пингует observer'а
Observer как message busObserver вызывается только для disputes и финального ack

6.5 Что НЕ решается v2

Honest limitations
  • Стоимость токенов — каждое invocation Claude перечитывает ledger + plan.md = ~30k токенов на холодный старт. Для 20 циклов это 600k токенов только на чтение. Можно митигировать кэшированием prompt'а на API-уровне.
  • Stuck loops — если Claude и Codex не могут договориться (Claude чинит, Codex re-opens, бесконечно), orchestrator должен после N итераций по одному finding'у эскалировать observer'у. В pseudocode это не показано — нужно добавить counter per-finding.
  • Race conditions — если Claude и Codex вызываются параллельно (не sequential как в pseudocode выше) — могут одновременно править ledger. Sequential invocation решает, но удваивает latency.
  • «Настоящая дискуссия» — даже в v2 нет real-time back-and-forth. Это всё ещё turn-based ledger workflow. Real conversation между LLM-агентами — отдельная нерешённая проблема.

7. Что осталось открытым

IDSeverityTitleOwner
F-004P1PlainCssFilePlan нужен discriminated union (mode: existing-owner | create-rule), runtime guard недостаточенclaude
F-005P1Tailwind + CSS Modules tie-breaker policy для same-property conflictobserver (product decision)
F-006P2cssSystem unions на остальных plan-типах (AdapterPropPlan, ScriptTemplateStylePlan) не сверены с полным CssSystemIdclaude
F-007P2Plan-7 / ArbitraryPropPlan: нет inline-комментария про empty requestedStyles, нет validation ruleclaude
F-008P2Plan validation rule про confidence mapping (deterministic / ai-assisted / fallback) не записана в plan.mdclaude
F-009P3Никаких failing тестов под спеку не написано — Phase 0 чеклист пустойcodex/claude
F-010P1InspectorValueCodec переопределить: только валидация + нормализация к inspector-form'е, без target-conversion. Per-target value mapping вынести в адаптеры (каждый адаптер знает value space своего target'а). Обновить Read/Write flow в plan.md и удалить примеры типа «opacity 50 → tailwind 50» из секции codec'а.claude

8. Action items

Немедленно (этой неделе)

  1. Закрыть F-004 (PlainCssFilePlan discriminated union) — 5-минутная правка plan.md.
  2. Принять решение по F-005 (Tailwind+CSS Modules tie-breaker). Рекомендация: CSS Modules выигрывает как explicit owner; Tailwind показывается как probable с диагностикой.
  3. Закрыть F-007 и F-008 — добавить комментарии и validation rules в plan.md.

В рамках следующего sprint'а

  1. Реализовать agent-loop.sh orchestrator + findings.yaml формат.
  2. Тест-драйв на следующей multi-agent задаче (можно начать с не-критичной — например, аудит ProseMirror integration plan).
  3. Если orchestrator работает — портировать workprocess.md и findings.yaml в шаблон, использовать на всех multi-agent специациях.

Долгосрочно

  1. Использовать Anthropic prompt caching для удешевления повторных чтений ledger.
  2. Расширить orchestrator до 3+ агентов (Claude + Codex + Gemini для tertiary view).
  3. Конвертировать spec examples в failing тесты (Phase 0 в plan.md) до начала имплементации — иначе спека не верифицируется.

9. График прогресса тестов

Динамика green-тестов с 2026-04-22 по настоящее время. Максимум: 2211 (полная E2E матрица). Отмечены ключевые события: коммиты с фиксами, старты прогонов, переломные моменты.

📊 Открыть полный интерактивный график