Унифицировать запись стилей инспектора между 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 отсутствует. Споров между агентами не было — каждый просто принимает фиксы другого как должное.
plan.md, Codex одновременно правил его же.RuntimeThemeContext противоречие в прозе. P1: CssFilePlan потерял required-field invariants после union refactor. P2: ScriptObjectStylePlan исключил Mantine.📍 Review Session 14:58 CEST с описанием изменений.PlainCssFilePlan всё ещё compile-valid-invalid (cascadeOwner? и createMode? оба optional с runtime guard). P1: Mixed Tailwind + CSS Modules tie-breaker всё ещё не определён.done, идёт спать. Claude не оповещён, не приходит фиксить. Convergence не достигнут — но никто этого не замечает.Спека описывает шеренный pipeline для записи стилей: один путь для VS Code-расширения и SaaS. Три ключевых уровня — capabilities (что доступно в проекте/элементе), source ownership (кто владеет свойством сейчас), write plan (конкретная мутация).
sourceForm: elementClass → TailwindPlan, cssStyleRule → CssFilePlan, scriptReactStyleRule → ScriptObjectStylePlan, scriptNativeStyleRule → ScriptTemplateStylePlan, adapterKnownElementProp → AdapterPropPlan, arbitraryElementProp → ArbitraryPropPlan. Per-target value conversion (opacity 50→0.5 и т.п.) живёт в адаптерных writer'ах, не в шеренном codec'е.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.
Theme — отдельная подсистема. Differentiates:
| Понятие | Что это | Где живёт |
|---|---|---|
RuntimeThemeContext | текущее предпочтение пользователя light/dark/system | per-request на StyleWriteContext |
| theme condition | условие в source: .dark &, data-theme='dark' | StyleCondition.theme[] |
| theme value owner | где определён цветовой токен | cascadeOwner на PlainCssFilePlan |
| write target | что мутировать: variable, branch, token | target.declarations |
Ключевое правило: system — это IDE preference, не source condition. Resolver приводит system → light или dark до роутинга.
| Решение | Суть |
|---|---|
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 system | shadcn/daisyUI — UI kits на Tailwind. Mantine/MUI/Chakra — могут быть и CSS system, и UI kit. |
StyleCondition extensible | state, 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. |
Протокол был задокументирован в workprocess.md. На бумаге выглядел разумно. На практике развалился по 6 конкретным причинам.
repeat-cadence (читает ledger каждые 10 минут). Claude — fresh session, инициируется только Alex'ом. Когда Codex добавляет findings — Claude об этом не узнаёт автоматически.
CssFilePlan (runtime guard вместо compile-time discriminated union) — Codex это заметил и снова открыл, но не написал «я не согласен с этим подходом, я бы сделал X». Просто log new finding, дальше ждать. Convergence через тишину, а не через консенсус.
Цель: исключить Alex'а как message bus. Сохранить asymmetric roles. Добавить convergence criterion и disagreement handling.
Дополнить 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."
#!/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
| Поломка v1 | Решение в v2 |
|---|---|
| Asymmetric agents | Симметрично: оба вызываются orchestrator'ом одинаково |
| No invocation channel | Orchestrator сам вызывает claude -p / codex exec |
| Session context loss | Каждое invocation короткое и focused (только open findings owned by you) |
| No convergence criterion | 3 quiet cycles подряд = converged |
| No disagreement protocol | Status dispute блокирует convergence, пингует observer'а |
| Observer как message bus | Observer вызывается только для disputes и финального ack |
| ID | Severity | Title | Owner |
|---|---|---|---|
| F-004 | P1 | PlainCssFilePlan нужен discriminated union (mode: existing-owner | create-rule), runtime guard недостаточен | claude |
| F-005 | P1 | Tailwind + CSS Modules tie-breaker policy для same-property conflict | observer (product decision) |
| F-006 | P2 | cssSystem unions на остальных plan-типах (AdapterPropPlan, ScriptTemplateStylePlan) не сверены с полным CssSystemId | claude |
| F-007 | P2 | Plan-7 / ArbitraryPropPlan: нет inline-комментария про empty requestedStyles, нет validation rule | claude |
| F-008 | P2 | Plan validation rule про confidence mapping (deterministic / ai-assisted / fallback) не записана в plan.md | claude |
| F-009 | P3 | Никаких failing тестов под спеку не написано — Phase 0 чеклист пустой | codex/claude |
| F-010 | P1 | InspectorValueCodec переопределить: только валидация + нормализация к inspector-form'е, без target-conversion. Per-target value mapping вынести в адаптеры (каждый адаптер знает value space своего target'а). Обновить Read/Write flow в plan.md и удалить примеры типа «opacity 50 → tailwind 50» из секции codec'а. | claude |
agent-loop.sh orchestrator + findings.yaml формат.Динамика green-тестов с 2026-04-22 по настоящее время. Максимум: 2211 (полная E2E матрица). Отмечены ключевые события: коммиты с фиксами, старты прогонов, переломные моменты.