История фаз
Неизменяемая запись завершённых фаз Brabo, перенесённая из CLAUDE.md 2026-08-23 без переписывания. ДЕЙСТВУЮЩЕЕ состояние продукта живёт в CLAUDE.md, в RN (docs/business-rules.md) и в ADR — этот файл отвечает на вопрос «как мы сюда пришли», а никогда не «что действует сегодня». Принятый ADR никогда не редактируется; исправления ADR, записанные в этом тексте, остаются действительными там, где они есть.
Статус
- ФАЗА 1 (MVP) — ЗАВЕРШЕНА: IAM/RBAC, сессии с неизменяемым event log, чат с маршрутизатором LLM (Ollama + API), metering/бюджет токенов, конвейер proposed_actions + permissions.json, движок Elixir/OTP с супервизией и событием завершения.
- ФАЗА 2 — ЗАВЕРШЕНА: GitProvider (Local/GitHub/GitLab) с контрактным набором тестов и capabilities, зашифрованные учётные данные, идемпотентный и возобновляемый через конвейер bootstrap Gitflow, мастер с прогрессом в реальном времени.
- ФАЗА 3 — ЗАВЕРШЕНА: Harness (PromptAssembler, ToolLoop, ContextManager, InstructionFiles, Hooks), явные handoffs, агенты Criativo, PO и Arquiteto с артефактами и проверками в домене.
- ФАЗА 4 — ЗАВЕРШЕНА: агенты выполнения (динамические dev по модулю в изолированных worktrees, QA и SecOps как gate-ы PR, проактивная Infra), блокировка защищённого merge, живая панель команды; настоящий Psicólogo и Anamnese с замкнутым циклом. Gate-ы проверены реальным выполнением (ADR 0020).
- ФАЗА 5 — ЗАВЕРШЕНА: образы production non-root и CI (ADR 0024), Kubernetes с Kustomize, HPA по очереди Oban, NetworkPolicies и ESO (ADR 0025), graceful shutdown + OpenTelemetry + dashboards (ADR 0026), бэкап с протестированным restore, runbooks и hardening (ADR 0027).
- ФАЗА DOC — ЗАВЕРШЕНА: docs/ как единственный источник по Diátaxis, сайт Docusaurus в website/, читающий из docs/, генерируемые справочники, docs/.docmap.yml + проверка дрейфа (docs:check) в CI, слой сообщества, проверенная MIT, механизм задокументирован в docs/explanation/documentation-workflow.md.
- ФАЗА 6 — ЗАВЕРШЕНА: механизированная политика веток (ADR 0030) — pr-police, approval-ladder с двумя режимами, promote/tag-release с вычисляемой версией и якорем по дереву, backmerge gate с автоматическим обратным распространением, версионируемые rulesets. Конвейер проверен от начала до конца (v0.1.0 → v0.2.0), а цепочка hotfix подтверждена реальным выполнением.
- ФАЗА 7 — ЗАВЕРШЕНА: first-party auth вместо Keycloak — argon2id, access Ed25519, ротация refresh с отзывом семейства, прогрессивный lockout, токены учётной записи и MailSender только в лог (ADR 0031, RN-030..033); атомарный переход с собственным эмитентом, RBAC нетронут, /internal/* с service token, cookie httpOnly + CSRF, миграция пользователей и полное удаление Keycloak (ADR 0032, RN-034/035); OpenAPI в 23 контроллерах с DTO, закреплёнными по типу, табличный тест, требующий метаданных, сгенерированный docs/reference/api/ с манифестом хешей (ADR 0033).
- ФАЗА 8 — ЗАВЕРШЕНА С ОТСЕЧЕНИЕМ: иерархия агентов — области с единственным lead
как внешним контактом, приватные внутренние делегирования (таблица
delegations,areaкак TEXT), консолидированный вердикт с прослеживаемостью, сбой с источником; QA как область (Lead + Автоматизация + Производительность/Безопасность, внешний контракт gate-ов не изменён — набор тестов Фазы 4 зелёный без изменений); Infra как область с субагентом Workflows, генерирующим CI по provider-у; панель команды по областям и Psicólogo/Anamnese, нацеленные на субагентов (ADR 0038). ОТСЕЧЕНИЕ ОБЪЁМА того времени, ОТМЕНЁННОЕ ФАЗОЙ 14d:agent_areas/agent_area_membersсуществуют с тех пор, по проекту, с настраиваемымmax_parallel— заставила это область dev, первая ДИНАМИЧЕСКАЯ. Отсечённым остаётся только бюджет по области (реальными пределами бюджета остаются проект, сессия и task). Список вapps/web/src/lib/agents.tsуцелел для цвета, иконки и ПРАВИЛА адресации; участники dev берутся из таблицы. - ФАЗА 9 — ЗАВЕРШЕНА: контрактный набор LLMProvider, запущенный
против существующих, база OpenAICompatibleProvider поверх
node:http с timeout БЕЗДЕЙСТВИЯ, ошибка, нормализованная по
code, двухслойные capabilities (ADR 0041); синхронизация каталога, где обнаруженная модель входит выключенной, исчезнувшая модель помечается, а не удаляется, цена заморожена вtoken_usageс append-only аудитом (ADR 0042); ModelPicker перегруппирован по происхождению с курированием. Шесть provider-ов 9b вошли через Фазу 11 (ADR 0043). - ФАЗА 10 — ЗАВЕРШЕНА: Bitbucket + GenericGitProvider сданы ЧЕРЕЗ
dogfooding (первое реальное выполнение, где Brabo строил сам себя,
в форке с ручным seed, партиями с перезапуском между tasks,
Criativo обязателен в цепочке). Итоги записаны в Фазе 12d, в
docs/explanation/primeiro-dogfooding.md — 17 находок с
arquivo:linha настоящие; КОЛИЧЕСТВЕННАЯ половина (перезапуски,
вмешательства, стоимость) осталась
não medido, потому что таблица наблюдений так и не была заполнена. Три P1 по операбельности закрыла Фаза 12. Остальные 14 остаются открытыми и перечислены в итогах — не исправляйте попутно. - ФАЗА 11 — ЗАВЕРШЕНА: шесть provider-ов Фазы 9b как конфигурация поверх
базы, каждый исследован с нуля по официальной документации (запрещено
наследовать quirk); LLM_PROVIDER_NAMES с 3 до 9; DTO учётных данных и
тестер подключения, выводимые из единого списка; capability
объявляется только доказанной, с двумя откатами вживую (DeepInfra и
Vultr); единственный новый hook в базе (
parseErrorFrame, +31 строка), доказанно необходимый для OpenRouter (ADR 0043). Незакрытые smoke-тесты с настоящими учётными данными: поглощены ФАЗОЙ 13. - ФАЗА 12 — ЗАВЕРШЕНА: операбельность после dogfooding, три находки
P1 закрыты и доказаны в одном выполнении (ADR 0047).
12a — принятие существующего репозитория: собственный маршрут,
getRepo, проверяющий доступ до записи,origin(created|adopted) в обеих таблицах, и ПЛАН как шлюз — dry-run, описывающий расхождение и ничего не меняющий, пока решение пустое; повторное принятие сходится (ADR 0044, RN-045/046). 12b — перепланирование dev agent по событию: сохраняемая машина состояний (working|awaiting_gate|idle|idle_tripped), реагирующая на два события существующей outbox,awaiting_gate, удерживающий worktree (который принадлежит АГЕНТУ, а не task), пока gate не закончит, и circuit breaker с явным re-arm (ADR 0045, RN-047). 12c — продвижение story с полномочиями пользователя:story_promotionпо проекту сmanualкак НОВЫМ значением по умолчанию (направленный backfill ставит существующие проекты вauto),proposed_readyкак предложение, а не состояние, продвижение, переиспользующее TransitionStoryUseCase (мёртвый код находки #13), и отказ, возвращающий к PO с причиной, закреплённой в его сессии (ADR 0046, RN-048). 12d — Noop вошёл в машину состояний 12b; скриптpnpm --filter api validacao:fase-12, который завершается != 0, когда критерий не выполняется, и извлекает улики из базы; итоги Фазы 10 записаны; docmap покрывает engine/dev и engine/agents. Незакрытое (выполнение проверки; объявленные ограничения Local/Noop): поглощено ФАЗОЙ 13. - Не рефакторьте готовое без явного запроса.
ФАЗА 13 — ЗАВЕРШЕНА 2026-08-07 (доказать по-настоящему и рассортировать)
Ни одной новой функции. Фаза закрывает объявленные незакрытые пункты, доказывает то, чего проверка Local/Noop объявленно не доказывает, и превращает 19 открытых находок в приоритизированный план. Усвоенный урок: ручная таблица Фазы 10 так и не была заполнена — в этой фазе КАЖДАЯ метрика извлекается скриптом из event log/token_usage, никогда не записывается вручную.
13a — Закрыть объявленные незакрытые пункты
- ЗАВЕРШЕНО 2026-08-07:
pnpm --filter api validacao:fase-12завершается0, а таблица event id находится в docs/explanation/validacao-fase-12.md (TODO(humano) ушёл). Это стоило четырёх исправлений — три в ИНСТРУМЕНТЕ (подопытный рождался в /tmp, невидимый для engine; Noop не открывал gate; Noop умирал наpr_settled) и одно в ПРОДУКТЕ (находка W: dev agent умирал, когда очередь пустела). - ЧАСТИЧНО 2026-08-07: smoke OpenRouter прошёл с настоящими учётными
данными и закрылся (реальная стоимость US$ 0,000005, прочитана из
token_usage). Остальные пять по-прежнему пропускаются — их ключей в среде нет. Smoke протух относительно ADR 0049 и никогда не запускался; исправлен заодно. Датированный результат — в docs/explanation/aceite-providers.md
13b — ЗАВЕРШЕНА 2026-08-07 (v2.4.0)
Вся цепочка доказана против НАСТОЯЩЕГО GitHub (daneiel/test), в десяти
выполнениях: удалённое принятие → план с решением пользователя → ручное
продвижение ОДНОЙ story → настоящий dev agent пишет код → commit, push и
УДАЛЁННЫЙ PR → gate открывается → область делегирует и освобождает с обоснованием →
субагент ПРИОСТАНАВЛИВАЕТСЯ на одобрении → отказ пользователя ВОЗОБНОВЛЯЕТ цикл →
вердикт changes_requested от LLM. Ноль перезапусков engine. Merge
исключён по замыслу (RN-014).
3. Сценарий — pnpm --filter api validacao:real, с фазами, разделёнными
по СТОИМОСТИ (--ate adocao|backlog|execucao) — дешёвая запускается одна
первой, и именно так ошибки конфигурации проявлялись до того, как
стоили денег. Требует выполнения ИЗНУТРИ контейнера api:
политика — файл в общем томе, а с хоста она рождается
в файловой системе, которую engine не видит.
4. pnpm --filter api medir:execucao — это инструмент, и он ДОКАЗАН:
сам воспроизводит числа, которые предыдущий dogfooding записал вручную
(18 вызовов, 292.211 токенов, ~US$ 0,03).
5. Результат — в docs/explanation/validacao-real.md, с десятью
выполнениями и тем, чему научило каждое.
СЕМЬ P1 закрыты по пути, все найдены ВЫПОЛНЕНИЕМ и ни одного — тестом: W (dev agent умирал при пустой очереди), Y (поиск не отличал пустое от ненайденного), AA (учётные данные git разрешались по тому, кто кликает, а не по тому, кто платит, — RN-082), AB (gate называл ожидающее решение сбоем инфраструктуры — ADR 0057), AC (перенаправление делало любую команду неодобряемой), ReDoS в области пути (CodeQL HIGH) и CVE postgrex.
Из находок, которые открыла фаза, X закрылась позже (ФАЗА 14d, RN-085 — предел итераций стал по ТИПУ агента). ДВЕ остаются открытыми, и их аргумент стоит больше чисел: Z и AD (allowlist глаголов НЕ сходится: глагол, форма и вызов — разные пространства, и выполнения 6/7/8 застряли по одному на каждом), и AE (агент QA пытается исправить код, который судит, вопреки собственному промпту, — сдерживается двумя независимыми барьерами, allowlist и областью). Ни одна из двух не баг для исправления: это продуктовое решение, и они — в docs/explanation/achados-execucao-real.md.
Вывод, который сдаёт фаза и который важнее PR: путь к автономии НЕ проходит через ослабление политики. Он проходит через то, что агент ЖДЁТ решения, а не умирает, — что ADR 0057 сделал для gate-ов, расширив 0052.
13c — ЗАВЕРШЕНА: сортировка стала docs/explanation/backlog.md
19 находок закрыты (19 из 19). Документ остаётся живым и получил новые находки 13b. 6. Сессия сортировки: прочитать docs/explanation/primeiro-dogfooding.md и классифицировать 19 находок в P1/P2/P3 с предложением группировки в связные фазы (по теме и зависимости между ними), оценкой относительной стоимости (P/M/G) и риском ожидания. Результат — ПРЕДЛОЖЕНИЕ: решение о приоритете — за пользователем. 7. Консолидировать полный backlog в живом документе (docs/explanation/backlog.md или эквиваленте): рассортированные находки + старые пункты — бюджет по области (ADR 0038; аппарат областей и Dev Lead вышли из backlog с ADR 0053, реализованным ФАЗОЙ 14d), ручной handoff выбранному агенту, MFA/social/OIDC/федерация (ADR 0031), настоящий SMTP в MailSender, deploy (DEPLOY_ENABLED + Environments), возврат rc/rcfix (ADR 0030), режим community в approval-ladder, «N агентов онлайн» в dashboard, предпочтение валюты с ручным курсом.
ФАЗА 14 — ЗАВЕРШЕНА 2026-08-07 (что потребовало реальное выполнение)
Выполнение hello world не прошло дальше первого хода и выявило дефекты, которые не ловил ни один набор тестов, потому что всё до этого работало с ЛОКАЛЬНОЙ моделью. Исправления уже внесены; остаётся функциональность, решённая пользователем во время выполнения.
14a — Операбельность агентов (ЗАВЕРШЕНА)
- Сбой хода стал ДОЛГОВЕЧНЫМ
agent.errorс источником, и агент ГОВОРИТ в нити; четыре разговорных агента перестали записывать пустойagent.response(RN-059). Путь ошибки, рассказанной в финальном frame-е, который не выдавал ни одного события, тоже закрыт. - Чат Criativo открывается приглашением, а не пустотой: он активируется и ждёт, пока пользователь заговорит первым, и экран теперь говорит об этом — в том числе что технология и код — НЕ его дело.
- Ключ, который тратит агент, — ключ OWNER-а workspace (RN-058). Раньше ход
искал учётные данные по SLUG агента в столбце UUID: ни один агент
никогда не использовал provider с учётными данными, и работал только
ollama. - Отчёт о расходах по ключам, только для владельца, с агентом, отделённым от человека (RN-060).
- Bootstrap Gitflow в GitHub: только что созданный репозиторий отвечает 409 во всём Git Data API, и первый commit проходит через Contents API. Поддельный backend тестов лгал (404 там, где GitHub отвечает 409) — исправлено заодно, именно это держало набор тестов зелёным при сломанном продукте.
14b — Древовидная временная шкала (ЗАВЕРШЕНА)
Одна ветвь на агента, от первой вехи до того, что он делает СЕЙЧАС, выведенная на 100% из event log, который экран уже получает. Хронологическая лента остаётся в столбце активности: она отвечает «что произошло», дерево отвечает «кто что делает».
Пересмотрено после ФАЗЫ 26 (без новой фазы, в том же духе точечного
исправления, что 12d/ПОСЛЕ-ФАЗЫ 15): критерий открытия по умолчанию сменился с одного лишь
«активен/стоит» на 5 агентов с самой СВЕЖЕЙ активностью — активный по-прежнему
в приоритете (RN-118); каждая свёрнутая ветвь получила собственный счётчик НОВИЗНЫ,
с «последним просмотренным» по агенту в read-state.ts (тот же механизм,
что у колокольчика, теперь гранулярный); а tool.call/tool.result/agent.response
стали раскрываться ПО ОТДЕЛЬНОСТИ (аргументы, результат, итерация), сгруппированные
по границе итерации ToolLoop.
14c — Автоматическая проверка UI (ЗАВЕРШЕНА)
Контраст стал тестом над design/tokens.css, с известным долгом, измеренным
и закреплённым; макет (обрезанный текст, меню вне viewport, dropdown, обрезанный
предком, цель < 24px) стал браузерным проверяющим в
scripts/dev/validacao-visual.js. Без новой зависимости.
14d — ЗАВЕРШЕНА: параллелизм решает LEAD, разрешаете вы
Сдана в четырёх PR (#181, #182, #183, #184). ADR 0053 стал ПРИНЯТЫМ, окончательно отменив три отсечения (Dev Lead, динамические области через module_map и общий аппарат областей).
- Области стали данными:
agent_areas/agent_area_membersпо проекту, сmax_parallelпо умолчанию 2. Заставила это область dev — первая ДИНАМИЧЕСКАЯ, чьи участники — по одному на модуль module_map. То, что не перечислимо в коде, должно быть данными. - Предел принадлежит СЕССИИ, а не модулю (RN-083). Счёт по модулю
позволил бы N модулей × 2 без какого-либо разрешения — прежняя дыра
под другим именем. Выше предела становится
proposed_action. - Настраивается по области в Настройках, требуя
maintainerпо той же причине, что и активация выполнения: изменить предел — значит решить, сколько продукт тратит, не спрашивая. - Anamnese предлагает повысить, когда разрешение стало рутиной, — три одобрения и НИ ОДНОГО отказа (RN-086). Один отказ гасит сигнал: если вы хоть раз отказали, предел делает свою работу.
- Dev Lead существует (RN-087), разговорный, получающий handoff
от Arquiteto в том же подтверждении, которое уже передаёт Infra, и
предлагающий план: сколько агентов на модуль и почему.
dev-<modulo>перестали быть адресуемыми через handoff — правило ADR 0038 стало действовать для dev так же, как уже действовало для QA и Infra.
Находка X закрыта здесь (RN-085): TOOL_LOOP_MAX_ITERATIONS
стал пределом ПО ТИПУ — 8 для разговорного, 60 для выполнения и gate. Критерий
того, кто поднимается, — НЕ «кто много работает»: это тот, у кого
под ним есть token_budget_micros, удерживающий расходы. infra-workflows
использует тяжёлый инструмент и остаётся на 8, потому что работает без бюджета.
Три пробела самой фазы, найденные мной при написании следующего среза, и все — один и тот же сбой: правило предела вошло с зелёными тестами, а продукт его не задействовал — ни один маршрут не вызывал вариант использования, одобрение действия ничего не выполняло, а тип мог быть автоматически одобрен через permissions.json (закрыто RN-086). Тестировать деталь — не значит тестировать путь к ней.
Вне объёма, по объявленному решению (ADR 0053, пункт 5): смена владельца кнопки
«Ativar execução». Делегирование Dev Lead → dev-<modulo>
через таблицу delegations с area = "dev", которое этот же пункт тоже
объявлял, ПЕРЕСТАЛО быть вне объёма — закрыто ADR 0094
(Аудит fluxo.yml × код, Волна 2, RN-405). Кнопка остаётся
обратимой; выполнение остаётся на текущем пути, а исправление после gate
по-прежнему идёт прямо к dev, открывшему PR.
ФАЗА 15 — ЗАВЕРШЕНА 2026-08-07 (gate-ы как данные)
Ни одного НОВОГО gate. Фаза выносит в docs/gates.yml gate-ы, которые УЖЕ
неявно существуют в продукте, с проверкой скриптом и серьёзностью,
в том же духе, что docs/.docmap.yml. Gate агента, которого не существует
(dev-lead, platform), входит как status: planned со ссылкой на
backlog — никогда как активный. Внешний контракт gate-ов НЕ меняется.
15a — ЗАВЕРШЕНА: декларативный реестр
- docs/gates.yml с существующими gate-ами (см. ADR 0054, расширяющий ADR 0048 — решение в event log делает прохождение gate измеримым), схема проверена тестом.
- Загрузчик в api, читающий реестр; внутренний endpoint чтения.
- Скрипт
pnpm --filter api validacao:gates, который извлекает из event log последнее прохождение каждого активного gate и завершается != 0, если у какого-либо gateblockнет улик проверки скриптом.
Не у каждого gate есть доказательство в event log, и реестр говорит, где оно живёт:
merge-protegida гарантирован ТЕСТОМ (блокировка в decide.ts не выдаёт
собственного события), а backmerge — это CI, с состоянием в .release/gate.json.
Поэтому каждый gate объявляет evidencia: event_log | teste | ci с
локатором — понизить их до warn значило бы солгать о самых
жёстких блокировках продукта.
15b — ЗАВЕРШЕНА: потребление
- Панель команды показывает gate, которого ждёт каждая story/PR, выведенный из реестра (не захардкоженный).
- docs/explanation/gates.md, объясняющий механизм, в docmap.
ПОСЛЕ ФАЗЫ 15 — ЗАВЕРШЕНО 2026-08-08 (v2.5.0 и v2.5.1)
Это не запланированная фаза: это то, что ИСПОЛЬЗОВАНИЕ нашло после 15, и оно здесь, потому что происхождение каждого пункта важно. Ничего из этого не пришло из сценария — пришло из навигации по приложению в Chrome и чтения панели безопасности.
Дефекты UI, найденные при навигации (RN-088..091):
429превращался в БЕЛЫЙ ЭКРАН, а приложение отвечало ещё большим трафиком. Правило стало общим: каждый экран различает ТРИ состояния — загрузка, ошибка (сtrace_idи кнопкой) и пусто.if (!dado) return nullсхлопывает все три.- Dashboard делал по запросу на проект: 3.824 → 12 req/min, с
GET /workspaces/:id/projects-summary. Колокольчик был недостающей половиной: 289 → один запрос за цикл (RN-090/091).
Ужесточение безопасности, с обеими панелями на НУЛЕ:
- ADR 0058 — закрытый CSP в api (
default-src 'none'), пересматривающий пункт 7 ADR 0027; иprojectId, ограниченный корнем (RN-092), что затрагивало не толькоpermissions.json, но и область, которая РАЗРЕШАЕТ терминал. - ADR 0059 —
GIT_OAUTH_STATE_SECRETбез значения по умолчанию в production (RN-093). Значение по умолчанию было ПУБЛИЧНЫМ (.env.example), и compose production подставлял его как fallback: требование «не пустая» ничего бы не поймало, потому что на реальном пути ошибки переменная была ЗАДАНА. - Алерт, который не выдерживал проверки, был ОТКЛОНЁН с письменным обоснованием, никогда не заглушён — и отклонение — решение пользователя, а не агента.
Три операционных урока, стоившие цикла CI, и они действуют как правило:
- Зелёный CodeQL в PR не доказывает, что старый алерт закрыт — проверка
PR сообщает о НОВОМ алерте. Алерт закрывается при сканировании ветки ПО УМОЛЧАНИЮ,
то есть только после того, как исправление дойдёт до
main. - Gitleaks сканирует commit-ы, достижимые из
refs/heads/*, а не дерево. Значение с высокой энтропией, закоммиченное однажды, навсегда проваливает CI на этой ветке; исправление поверх НЕ очищает. Выход — сделать commit недостижимым (пересобрать изdevи УДАЛИТЬ старую ветку ДО открытия нового PR) — никогда allowlist, который ослепляет сканер на этом пути для всех commit-ов. Refs PR в область не входят. Fixture, представляющий секрет, рождается БЕЗ энтропии. - Барьер, живущий в другой функции, CodeQL не видит — отсюда
три
js/path-injection, пережившие исправление. Проверка осталась централизованной (RN-092), и цена была уплачена на панели: дублирование её в каждом вызывающем было бы проверкой, которая однажды разойдётся.
ПРОГРАММА 16–26 — что попросила навигация (в работе)
Одиннадцать запросов, родившихся из ПЕРВОЙ реальной навигации по приложению после сброса базы. Это не сценарий: это использование. Три открытия расследования определили размер и ПОРЯДОК программы, и стоит записать каждое, потому что ни одного не было в исходном запросе:
- В
design_handoff_brabo/есть ПОЛНЫЙ дизайн-handoff — 8 экранов высокой точности, токены, типографика и бренд. Решено принять его ЦЕЛИКОМ и ПЕРВЫМ: так одиннадцать запросов рождаются в новом визуале, а не делаются дважды. - Терминал вкладки Code стал решением АРХИТЕКТУРЫ. У каждого проекта теперь
свой контейнер, и образ решает ARQUITETO. Внутри него
агент свободен;
git push, PR и deploy остаются за людьми. Это погашает самый большой открытый долг продукта — сегодня агент выполняется в ТОМ ЖЕ контейнере, что и монорепозиторий Brabo, а ADR 0055 говорит о себе, что он политика, а не изоляция. - По пути проявились два настоящих дефекта, и ни один не был запрошен:
agent_areasНИКОГДА не записывается (upsertбез вызывающего, API возвращает[]), а ящик колокольчика сортируетseq ASCв SQL — показывает САМЫЕ СТАРЫЕ.
Предел выполнения: не более ЧЕТЫРЁХ субагентов параллельно. Фазы были
нарезаны по ОСПАРИВАЕМОМУ ФАЙЛУ, а не по теме — именно это делает параллелизм
возможным. Ограничитель backend-а — apps/api/src/db/migrations/meta/_journal.json
и снимки drizzle: ОДНА миграция на волну, потому что ручное разрешение конфликта
снимка — путь к расходящейся схеме. Диапазоны RN и номера ADR
ЗАРАНЕЕ ВЫДЕЛЯЮТСЯ до начала волны: две параллельные фазы, пишущие
RN-094, заставляют одну перенумеровываться при merge, а перенумерованная RN ломает ссылки
#rn-0xx, которые проваливает pnpm docs:check.
ФАЗА 16 — ЗАВЕРШЕНА: фундамент (разблокировать параллелизм)
Ни один из одиннадцати запросов здесь не сдаётся. Фаза существует, потому что три из них затрагивают
ТЕ ЖЕ четыре точки router.tsx/ProjectPage.tsx, а ещё три
упираются в отсутствие общей детали. Без неё волны схлопываются в последовательное
выполнение.
design/tokens.cssполучает--violet(агенты/ИИ) и всё прочее, что определяет handoff. Шрифты ОСТАЮТСЯ self-hosted — следование handoff здесь сломало бы приложение под CSP nginx (ADR 0036). Тест контраста покрывает новые пары, а известный долг в 4 пары остаётся закреплённым.- Вкладка перестаёт быть закрытым списком в ДВУХ местах: единый реестр, из которого
выводятся
PROJECT_TABS,type TabKey, линейка и render. Тест, проваливающий ключ, присутствующий в одном месте и отсутствующий в другом, — это настоящий дефект. Disclosureв дизайн-системе, с семантикой самой полной уже существующей реализации (ModelCatalogSection). ТОЛЬКО КОМПОНЕНТ: ни одна из шести ad-hoc реализаций здесь не мигрирует, иначе фаза открыла бы три самых оспариваемых файла программы.- Метка сессии становится helper-ом, и пять inline
slice(0, 8)мигрируют. Именно эта миграция не даёт ФАЗЕ 20 столкнуться с 19. CLAUDE.mdвходит в определение готовности (см. раздел Документация), с правиломwarnв docmap. СДАНО: правило —claude-mdв docs/.docmap.yml, срабатывающее отpackage.json,pnpm-workspace.yaml,apps/*/package.json,apps/engine/mix.exs,docs/adr/*.mdиdocs/gates.yml— фраза «сегодня у него НОЛЬ покрытия», которая была здесь, описывала состояние ДО фазы и осталась лгать после неё.
ФАЗА 17 — ЗАВЕРШЕНА: 8 экранов по handoff
Визуальная точность ДО поведения, чтобы не переделывать работу. Ни одно
бизнес-правило не меняется: если экрану нужны данные, которых нет, это становится
объявленным незакрытым пунктом, а не попутной функцией.
6. Вход и App/список проектов.
7. Проект и Сессия, с сохранением трёх состояний RN-088 (ошибка до пустоты).
8. Одобрения и Настройки — Одобрения являются визуальной базой ФАЗЫ 19.
9. Доказательство, что точность достигнута: контраст, измеренный на токенах, и
макет, проверенный scripts/dev/validacao-visual.js на ВСЕХ затронутых
экранах. .dc.html — эталон, а НЕ код для копирования — это говорит
сам README handoff-а.
ФАЗА 18 — ЗАВЕРШЕНА: область существует в базе (дефект, исправлен раньше)
- У
AgentAreaRepository.upsertНЕТ ни одного вызывающего, поэтому четыре варианта использования работают над пустой таблицей. Provisioning при создании проекта + backfill, с тестом, доказывающим, что только что созданный проект ИМЕЕТ области, — это тот же сбой, что в ФАЗЕ 14d: тестировать деталь — не значит тестировать путь к ней. - Схлопнуть ТРИ копии списка областей (api, web, engine) в один источник.
ФАЗА 19 — ЗАВЕРШЕНА: одобрение, которое читается
- Убить общий fallback
ApprovalCard, который вываливаетchave: JSON.stringify(valor)— вероятная причина «трудно читать». Каждый тип получает ФРАЗУ на pt-BR; тип без фразы показывает глагол + «ver detalhes», а сырой payload рождается СВЁРНУТЫМ, никогда не вываленным. - Свёртывание в ТРЁХ местах: Одобрения, Insights и карточка в чате, с глаголом и фразой из ОДНОГО модуля.
- Ограничение проекта: свёртывание НЕ вводит новый обязательный prop в
ApprovalCard. Именно это оставляетSessionPage.tsxнетронутым и убирает ребро с ФАЗОЙ 20.
ФАЗА 20 — ЗАВЕРШЕНА: сессия получает идентичность
sessionsполучаетkindиnameв ОДНОЙ миграции — две миграции над одной таблицей сталкиваются в journal и снимках.- Согласовать с выводом по событию:
kindклассифицирует НАМЕРЕНИЕ создания,execution.activatedпродолжает классифицировать СОСТОЯНИЕ выполнения, и ни одно не перезаписывает другое.execution.activatedв консультативной сессии — явная ошибка, а не молчаливая конвертация. - Переименование с сохранением хэштега; без имени деградирует до одного хэштега.
- Кнопка возврата к dashboard — сегодня
SessionPage.tsxне импортирует ниLink, ниuseNavigate, и НИ ОДНА навигация не выходит с экрана. Назначение пересмотрено в более позднем раунде: кнопка возвращает к ПРОЕКТУ сессии (/projects/:projectId), а не к корневому dashboard — сессия всегда рождается внутри проекта.
ФАЗА 21 — ЗАВЕРШЕНА: объём событий (RN-099/100)
useSessionEventsостаётся ТЕКУЩИМ СОСТОЯНИЕМ (latest, четыре потребителя: roster, дерево, выполнение, Одобрения). ИСТОРИЯ сталаuseSessionEventHistory, только для Активности, сafterSeq, который endpoint уже возвращал. Ни нового маршрута, ни нового параметра. Якорь — ХВОСТ, а не начало сессии: endpoint пагинирует вперёд, иbeforeSeqне существует, но открыть ленту на событии № 1 сессии из тысяч — значит выдать не тот экран. Поэтому первая страница — то же чтениеlatest(ТОТ ЖЕqueryKey, дедуплицированный), и каждый клик спускается на фиксированное окно назад.- Колокольчик сортирует
DESCв SQL. Отсечка «прочитано» НЕ сменила семантику, и поэтому ADR нет: отсечка поseqпомечает ПРЕФИКС, а ящик показывает СУФФИКС, так что «пометить 50 показанных» невыразимо без таблицы прочитанных по событию. Изменилось то, что ящик перестал скрывать то, что продвижение поглощает, — итог по проекту,+ N mais antigosи кнопка, говорящая, сколько она помечает. Недостающее число получается ВЫЧИТАНИЕМ (latestSeqминус отсечка), а не запросом. - Экономия RN-090/091 не регрессировала: один запрос на цикл, а
первое «загрузить более старые» стоит НОЛЬ — чтение
latestприносит 200, а окно показывает 100. У старой страницыstaleTime: Infinityи нетrefetchInterval: закрытое окноseqнад неизменяемым событием не меняется.
ФАЗА 22 — ЗАВЕРШЕНА 2026-08-09 (расходы для двух аудиторий)
Сдана как вкладка Gastos (ADR 0063, RN-101).
22. Недостающие АГРЕГАЦИИ вошли в один метод —
sumGroupedBy(dimensao, escopo) с model, project, actor,
session и day. provider НЕ является его измерением, и отсутствие
структурное: разбивка по provider-у — это разбивка по УЧЁТНЫМ ДАННЫМ, и именно это
не даёт представлению участника случайно получить эту ось — нет
аргумента, который можно передать.
23. Без библиотеки графиков: дневные столбцы и рейтинг во встроенном SVG. Дневной
ряд приходит из api ПЛОТНЫМ (день без расходов входит с нулём), иначе три
вызова за три недели превращаются в три слипшихся столбца.
24. Коллизия решена разделением ВОПРОСОВ, а не фильтров.
Отчёт по УЧЁТНЫМ ДАННЫМ остаётся исключительным для владельца (RN-060) и получил
рядом разбивку workspace по модели/проекту/актору/дню, тоже
owner. Представление участника — по АКТОРУ, в токенах и оценочной стоимости,
без provider-а и без учётных данных — и актор берётся из аутентифицированного ТОКЕНА,
без параметра, куда можно вписать id другого человека. Агент не входит в
счёт участника: token_usage записывает, кто потратил, а не кто велел
тратить.
Без миграции, но С измерением: на 525 тысячах строк запросы выполняются за
55 ms и 38 ms seq scan-ом, а индекс по token_usage(created_at)
довёл бы их до 32 ms и 19 ms. Число — в ADR; индекс войдёт в волну, у которой
будет слот.
ФАЗА 23 — ЗАВЕРШЕНА 2026-08-09 (модель, наследуемая по области)
- Область
areaвошла в каскад (sessão > agente > área > projeto > workspace), МЕЖДУ агентом и проектом: это шаблон, который разделяют lead и субагенты, а агент расходится, перекрывая его (ADR 0064, RN-102). - Несогласованность решена ДО написания кода: binding агента был ГЛОБАЛЬНЫМ
(
scope_id= чистый slug,:projectIdмаршрута игнорировался с Фазы 9a), а область — по проекту начиная с ADR 0053. Проектная область над глобальной заставила бы одного и того же агента разрешать разные модели только там, где есть область. Решение: binding агента тоже стал по проекту —scope_idдляagent/areaстал составным,<projectId>:<slug|chave>(RN-103), — а не область под агентом, что противоречило бы «наследуемому шаблону» (почти каждый binding сегодня — агентский). - UI показывает, кто НАСЛЕДУЕТ и кто РАЗОШЁЛСЯ (
AreaModelsSectionи столбец Origem вModelsSection, вProjectSettingsTab.tsx); «вернуться к наследованию» — этоDELETEbinding-а агента/области, никогда не запись в него модели нижнего уровня — копирование стало бы копией, которая сама разойдётся при следующем изменении области. Рольmaintainerдля смены модели области (та же линейка, что у предела параллелизма, RN-083); для агента по-прежнемуdeveloper.
ФАЗА 24 — ЗАВЕРШЕНА: Chat и Criativo как места (RN-104)
- Две вкладки на экране ПРОЕКТА, каждая перечисляет сессии своего
kindи создаёт в этомkind, не спрашивая снова. Сессия остаётся отдельным экраном — вкладка не стала контейнером чата. - Коллизия решена тем, что «Sessões» УШЛИ с линейки. От неё осталась
КЛЮЧ deep-link: Chat несёт
key: 'sessions', и именно это заставляет старый?tab=sessionsоткрываться в Chat с ОТМЕЧЕННОЙ вкладкой. Alias, разрешённый только в панели, открыл бы нужную панель и оставил линейку без выделения, потому чтоTabsсравниваетactiveсkey, аactiveпишетProjectPage— один из двух файлов, которые волна держала закрытыми. - Вердикт по «Iniciar ideação»: она ОСТАЁТСЯ. Половина проблемы уже
закрылась в ФАЗЕ 20 (кнопка исчезла из консультативной сессии, RN-097), а другая
половина не была избыточностью: именно она приводит Criativo, и с этого момента
тратится ключ владельца (RN-058). Неверным было МЕСТО —
приглашение тратило абзац, указывая на topbar. Теперь действие живёт
ВНУТРИ приглашения, пока оно на экране, и возвращается в topbar, когда его
нет;
conviteVisivel— единственный вопрос, который они разделяют, чтобы оно не появлялось дважды и не исчезало.
ФАЗА 25 — ЗАВЕРШЕНА С ОТСЕЧЕНИЕМ: контейнер на проект (граница перестаёт быть только политикой)
Самое большое архитектурное изменение программы, и то, что гасит долг, на который Фазы B
и F уже указывали по отдельности. Сданы 25a (решение Arquiteto и
шлюз) и 25c (граница внешнего эффекта); 25b (жизненный цикл
контейнера) остался ОТСЕЧЁННЫМ и объявленным — не реализованным наполовину (ADR 0065, RN-105
и RN-106).
30. ЗАВЕРШЕНО: ARQUITETO решает, какой образ поднимается для проекта, как
свой артефакт (artifact.project_image в event log, версионируемый, без
таблицы) — проверяемый, а не скрытая конфигурация. Пока он не
решил, вкладка Code отвечает 409 и не открывается (RN-105).
31. НЕ СДАНО В ТО ВРЕМЯ, объявлено: жизненный цикл по проекту (provisioning,
перезапуск, очистка, реально применяемый предел ресурсов), с worktree
агента, живущим внутри контейнера. Состояние контейнера ИЗМЕНЯЕМО и
требовало собственной таблицы — импровизация в event log лишь бы не ждать
слота миграции породила бы исправление сразу же после.
ЧАСТИЧНО ОТМЕНЕНО Волной 4/фронтом F1 ПРОГРАММЫ 28: ТАБЛИЦА
состояния (project_containers, ADR 0081) появилась. По-прежнему не хватает,
без смягчения, половины, которая делала таблицу хоть чего-то стоящей: ни один
настоящий оркестратор ещё не вызывает Docker, и worktree агента остаётся вне
контейнера. Половина «внутри агент свободен» политики терминала
ЕЩЁ НЕ изменилась — ADR 0055 продолжает действовать как есть, пока контейнер
не поднимется по-настоящему.
32. ЗАВЕРШЕНО В ТО ВРЕМЯ, ПЕРЕСМОТРЕНО локальным runner-ом (ADR 0102/0103, RN-418):
граница внешнего эффекта. git push, PR и deploy не выходят через
терминал — даже внутри области проекта, — и правило БЫЛО deny
(не require_approval, из-за «sempre permitir»), с
сообщением, перенаправляющим к ТИПИЗИРОВАННОМУ действию (git_push/git_merge/
pr_open), которое рождается proposed_action (RN-106, пересмотрена). deny
стал АБСОЛЮТНЫМ ПРЕДЕЛОМ (безусловный require_approval, никогда не
автоодобряемый) — решение владельца продукта, «sempre permitir» закрыт
в источнике, чтобы не стать декоративным (см. раздел о локальном runner-е). Merge в защищённую
по-прежнему ручной (RN-014), нетронут. Сеть и расходы имеют собственный вердикт в
АРТЕФАКТЕ образа (network: none/egress, предел cpus/памяти/pids,
который отказывает, а не молча понижает) — решено ОДИН раз, а не
по команде, по той же причине, по которой Z и AD доказали, что allowlist
глаголов не сходится. Само закрытие Z и AD зависит от 25b (физической
стены), которая ещё не поднялась.
ФАЗА 26 — ЗАВЕРШЕНА: Code, только чтение
-
ЗАВЕРШЕНО (26a):
GitProviderContractполучилlistTreeи diff PR (11-я/12-я операция), capability объявлена ТОЛЬКО будучи доказанной набором тестов. -
ЗАВЕРШЕНО (26b): четыре маршрута
code.controller.ts— дерево, файл, поиск и diff — за ЦЕНТРАЛИЗОВАННОЙ проверкой RN-092/095, с пределом и кешем в поиске (который СОСТАВНОЙ, а не операция контракта). -
ЗАВЕРШЕНО: экран (
ProjectCodeTab/code/*), зарегистрированный вproject-tabs.tsбез касанияrouter.tsx/ProjectPage.tsx. Rail (Explorador/Buscar — единственные с реальными данными), проводник, загружаемый по КАТАЛОГУ, вкладки редактора с breadcrumb, реальный поиск сfilesScanned/truncated, diff PR по известному id (сpatch: null, трактуемым как «нет текста», никогда «нет изменения»). Подсветка синтаксиса — СОБСТВЕННЫЙ токенизатор на регулярных выражениях (code/highlight.ts) — НОЛЬ новых зависимостей, против 15–90 KB Prism/highlight.js/Shiki для того, что нужно вкладке; три новых цветовых токена (--syntax-function/ -comment/-operator), откалиброванных на 4,5:1 против--code-bgв ОБЕИХ темах. Четвёртое состояние RN-088 — заблокировано ожидающим решением Arquiteto (RN-105) — стало RN-107, запрашиваемым ДО попытки читать код, с собственным повторным запросом каждые 15s, пока заблокировано.ОБЪЯВЛЕННЫЕ незакрытые пункты, без реальных данных за ними: интерактивный терминал (ФАЗА 25b, которая остаётся отсечённой, — честное пустое состояние во вкладке), blame, список PR внутри вкладки (diff достижим только по известному id, пришедшему из Одобрений) и панель Проблем/lint/тестов. Виртуализация строк тоже осталась за бортом в то время — сам handoff называет вкладку кода «самой дорогой в программе»; предел 512 KB на файл (
GIT_BLOB_MAX_BYTES) до тех пор ограничивал худший случай. ЗАКРЫТО Волной 4/фронтом E2 ПРОГРАММЫ 28 (RN-239..242): только видимое окно становится узлом DOM, а миникарта переиспользует ту же токенизацию без второго прохода.
ФАЗА 26b — ЗАВЕРШЕНА: фундамент blame, навигируемые PR и богатая ветка
Фаза началась как ТОЛЬКО слой API, и фраза «никакого UI», оставшаяся здесь,
описывала состояние ПЕРВОЙ партии — она устарела и начала учить неверному.
ВСЕ ТРИ UI существуют: богатый dropdown веток (пункт 38, CodeBranchPicker.tsx),
переключатель blame по требованию (CodeEditor.tsx, RN-113) и список PR внутри
вкладки (CodeDiffPanel.tsx), последние два сданы в v3.0.0. Три
объявленных незакрытых пункта — blame,
богатый dropdown веток и список PR — затронули бы ТЕ ЖЕ файлы
(контракт, три provider-а, вариант использования, контроллер, контрактный
набор), если бы их атаковали по отдельности параллельные агенты; решение было
сдать все три вместе, разом, и оставить UI каждого трём
следующим агентам без риска коллизии.
36. GitProviderContract получил blame, listPullRequests и
listBranchesDetailed (13-я, 14-я и 15-я операции — RN-110/111/112),
capability объявлена только доказанной контрактным набором против
трёх provider-ов (local против настоящего git; github/gitlab
с mock — настоящие smoke-тесты по-прежнему пропускаются без
GITHUB_TEST_TOKEN/GITLAB_TEST_TOKEN в среде, та же ситуация, что в
ФАЗЕ 13a). listBranchesDetailed — СОБСТВЕННАЯ операция, а не расширение
listBranches (RN-112) — обогащение стоит лишнего вызова provider-а
НА КАЖДУЮ ВЕТКУ, а listBranches остаётся операцией bootstrap Gitflow,
который эту цену не платит.
37. Три новых маршрута в code.controller.ts (GET .../code/blame,
GET .../code/pull-requests, GET .../code/branches), тот же role: viewer и та же центральная проверка пути (RN-095), что у четырёх
предыдущих. apps/web/src/lib/api-client.ts/api-types.ts получили
соответствующие функции и типы (getCodeBlame, getCodePullRequests,
getCodeBranches) для потребления следующей волной — CodeShell.tsx и
CodeDiffPanel.tsx НЕ тронуты сверх комментария, документирующего,
что фундамент уже существует.
38. ЗАВЕРШЕНО: богатый dropdown веток (RN-112) — CodeBranchPicker.tsx,
открываемый из CodeShell.tsx, заменяет простое текстовое поле
селектором, перечисляющим каждую ветку с ahead/behind относительно ветки по умолчанию и
связанным PR, если он есть (aberta/mesclada/fechada). Ref вне
списка (tag или sha) остаётся достижимым — listBranchesDetailed не
перечисляет эти два, поэтому в подвале dropdown есть ручное поле.
Заморозка программы: каждая фаза объявляет, чего она не делает, и самое жёсткое —
у 26: ТОЛЬКО ЧТЕНИЕ кода, никакого сохранения через вкладку. Редактирование — следующая
фаза, и когда она придёт, запись — внешний эффект: рождается proposed_action.
ПОСЛЕ ПРОГРАММЫ 16–26 — что использование попросило потом (RN-148)
Тот же дух, что ПОСЛЕ ФАЗЫ 15: это не запланированная фаза, это то, что попросило ИСПОЛЬЗОВАНИЕ.
Истории с одновременно ожидающим продвижением (RN-048/RN-126) превращались в N
отдельных карточек на временной шкале сессии PO, когда он производил несколько за одну
партию. Carousel (apps/web/src/components/ui/Carousel.tsx) — первый
компонент поэлементной навигации в дизайн-системе: 2+ ожидающих
становятся ОДНОЙ навигируемой каруселью (стрелки, точки, клавиатура), где «Aprovar todas»
продвигает всю партию одним вызовом (promoteStories уже был пакетным,
без изменения контракта); 1 ожидающая остаётся привычной простой карточкой.
Решение о том, когда формируется «партия», пересматривается при каждом render — это
множество ЕЩЁ ОЖИДАЮЩИХ продвижений в сессии, а не «созданных подряд
без перерыва», — чтобы разрешение истории посреди карусели
само пересчитывало партию, без собственного состояния для синхронизации.
Проект Local или Container (RN-169/RN-170, ADR 0072). Запрос владельца
продукта: «перейти границу записи кода только в контейнере и иметь возможность
писать код из папки пользователя», с вариантом свободно набираемого
пути, выбранным им, с пониманием, что это работает, только будучи смонтированным в
контейнер. projects получает (workspace_mode, workspace_path) в миграции
0043, с container по умолчанию — для того, кто не выбирает, ничего не меняется. ADR
0072 ПЕРЕСМАТРИВАЕТ часть 0065 и затрагивает территорию 0055, которые решили обратное
направление (стену контейнера), и поэтому следствие записано без
смягчения: СТРУКТУРНОЕ сдерживание join(env, coluna) — ни один
повреждённый столбец не выходит из управляемого корня — перестаёт существовать для проекта Local, и
остаётся защита при создании плюс лексическая повторная проверка при чтении.
ФАЗА 25b остаётся отсечённой: проект Local работает в ТОМ ЖЕ контейнере, что и сегодня, изменилась
только папка.
Диаграмма C4 Arquiteto (RN-149, ADR 0068). Новый результат:
create_c4_diagram генерирует Context + Container (Simon Brown) в Mermaid,
отображаемые в Обзоре. Уровень Container ВЫВОДИТСЯ из действующего module_map
вариантом использования — никогда не перепечатывается моделью, — чтобы не открывать
второй источник, расходящийся с первым; только Context (имя системы и
внешние акторы) приходит из tool call. artifact.c4_diagram — артефакт без
таблицы, версионируемый в event log, тот же замысел, что у artifact.project_image
(ADR 0065). mermaid вошёл как новая RUNTIME-зависимость
apps/web — первая такого рода (сайт документации уже использует Mermaid, но на
этапе build) — изолированная за apps/web/src/lib/mermaid-render.ts с
динамическим import(); vite build подтвердил, что только entrypoint загружается
сразу, а тяжёлые chunk-и Mermaid остаются по требованию, и ни один из них не использует
eval/new Function — закрытый CSP из ADR 0058 остался нетронутым, без
изменения конфигурации.
РАУНД exp003 — ЗАВЕРШЁН (RN-150..162, PR #278–#293)
Это не фаза: это 17 пунктов, которые ИСПОЛЬЗОВАНИЕ нашло в живой тестовой сессии
над проектом exp003, исправленные пятью последовательными волнами по
ОСПАРИВАЕМОМУ ФАЙЛУ — четыре из них сходились в SessionPage.tsx, что
вынудило сериализовать. Важно происхождение: ни один из них не был найден тестом.
Чему научил раунд, и это стоит больше списка: самое большое исправление
(RN-155) было невидимо для любого набора тестов, потому что дефект был в сравнении
двух РАЗНЫХ числовых пространств, которые случайно оба number, —
event.seq (без пропусков, по СЕССИИ) против action.seq
(proposed_actions.seq, глобальный bigserial ВСЕЙ таблицы, общий
для всех проектов и всех сессий системы и задокументированный в схеме как
намеренный контраст). Порядок казался правильным, лишь пока таблица была
маленькой. Исправление не выдумало поле: оно использовало событие
proposed_action.created, которое ProposeActionUseCase уже записывает в ТОЙ ЖЕ
транзакции, с payload.actionId, указывающим обратно.
Engine и сдержанное чтение (RN-150) — search_workspace возвращал ВСЕ
совпадения, без предела: вторая настоящая причина 413 при ревью PR, после
исправления read_file. Два НЕЗАВИСИМЫХ предела, потому что поиск
переполняется двумя способами — количеством совпадений (SEARCH_WORKSPACE_MAX_HITS,
500, над Stream, который ПРЕКРАЩАЕТ сканировать на max_hits + 1) и байтами
итогового текста (SEARCH_WORKSPACE_MAX_BYTES, 32.768). Собственные переменные, не
переиспользованные из двух других: тот же класс переполнения, но расхождение одной не
должно требовать трогать другие. Метка усечения адресована МОДЕЛИ
(велит уточнить термин) и никогда не выдумывает итог, потому что подсчёт итога
снова оплатил бы I/O, ради экономии которого существует предел.
Числа, которые лгут (RN-151) — бейдж проекта в боковой панели считал
ЛЮБОЕ непрочитанное событие: exp003 показывал 392, тогда как вкладка Одобрения
того же проекта показывала 8. Число, которое не соответствует ничему исполнимому
при клике, хуже, чем никакое. Стало pendingApprovalsCount в уже
существовавшей read model (RN-090), одной агрегацией больше в Promise.all — никогда
N+1.
Прослеживаемость того, что произвёл агент (RN-152) — ветка
feature/task-XXXXXXXX в dropdown вкладки Code теперь говорит, какому dev
agent и модулю она принадлежит. Модуль берётся из действующего module_map ТЕМИ ЖЕ
функциями, что его сгенерировали, никогда обратным regex по имени; без связи
деградирует до null, а не угадывает.
Auto mode (RN-153/154, см. Соглашения) — подстановка actionType: "*".
Важно, где она живёт: разрешение принадлежит РЕПОЗИТОРИЮ (findMode),
а decide.ts не получил ни строки — именно это делает три абсолютных
предела истинными по ПОСТРОЕНИЮ, а не ещё одним if, который однажды
кто-то забудет.
Нить сессии, которая читается (RN-155..159) — упорядочивание (выше);
индикатор 5s с фиксированным текстом «Reunindo informações...» вместо
подстановки агента, который уже появляется в заголовке (RN-156); создание
эпика/истории PO, превращающееся в компактное уведомление, а не в пузырь с тем же
визуальным весом, что у настоящего ответа (RN-157); лёгкий Markdown с подсветкой
в чате собственным парсером на регулярных выражениях, ноль новых зависимостей и ни одного
dangerouslySetInnerHTML, причём отображается только ВЫВОД агента — то, что
набирает человек, остаётся буквальным (RN-158); и «Artefatos gerados», сгруппированные по
агенту с Disclosure из RN-138, каждый артефакт ведёт туда, где он
живёт (RN-159). module_map/C4 остались ВНЕ по зафиксированному решению: это ДЕЙСТВУЮЩЕЕ
состояние проекта, а не артефакт, датированный сессией.
Порядок полномочий (RN-160/161, ADR 0069) — «Confirmar arquitetura
pronta» включается только при хотя бы 1 ПРОДВИНУТОЙ истории (недостаточно иметь
захваченное правило); а принятие handoff для Dev Lead цепляет активацию
выполнения, когда эффективная роль принимающего уже это разрешает. Слияние — только на
клиенте: backend по-прежнему требует maintainer (RN-137), поэтому оно избавляет
от лишнего клика, не сдвигая границу авторизации.
Структурированный ввод (RN-162) — ask_structured_questions у Criativo:
модель объявляет вопросы в схеме, форма отображается
StructuredQuestionCard, а ответ ПЕРЕИСПОЛЬЗУЕТ SendAgentMessageUseCase,
а не открывает второй путь сообщения. Ответ — однократный акт:
повторная отправка — 409, а не перезапись.
РАУНД exp001 — Criativo выполняет обещание (RN-163)
Тот же дух, что у других раундов: пришло из ИСПОЛЬЗОВАНИЯ, а не из сценария. Жалоба была:
«Criativo не ответил после того, как сказал, что исправит и попробует снова»,
и фраза буквально была в коде — run_turn_capturing/1 вызывал модель ОДИН
раз, отправлял инструменты и возвращался. Результат инструмента попадал в
историю в памяти, и никто его больше не читал, поэтому обещанное исправление
происходило, только если пользователь присылал другое сообщение.
Criativo был единственным разговорным агентом без цикла tool use; он получил такой же, как у
PO, с собственным пределом 12. Что важно сверх исправления: решать, что
объявляется, стал предел, а не фиксированный текст — обещание повторной попытки
входит во фразу, только когда ещё есть ход, чтобы его выполнить. Исчерпанный предел стал
рассказанным agent.error (po_server в этом случае заканчивал молча, и это был
следующий очевидный долг — закрытый чуть ниже RN-166), а сбой
инструмента перестал быть agent.response
— в event log он был неотличим от обычного ответа и не называл
никакого источника, ровно то, что RN-059 закрыла для сбоя хода.
РАУНД exp001 — PO читает уже существующее, и с него спрашивают (RN-164..166)
Это не фаза: это дефект, найденный реальным ИСПОЛЬЗОВАНИЕМ, и происхождение важно, потому что ни один набор тестов его бы не поймал. Backlog вышел с эпиком и ни одной историей — значит, без задачи, значит, выполнение застряло без какой-либо ошибки. Расследование нашло четыре наложенные причины, и первые три — одна и та же: PO не мог знать и не мог предупредить.
- У PO было четыре инструмента, и все — ЗАПИСИ. Контекст
собирался ОДИН раз, на kickoff, из 200 последних событий СЕССИИ —
с этого момента он больше ничего не перечитывал: не знал, какие правила существуют,
какие он уже покрыл и что сам уже создал. Вошли
listar_regras_de_negocioиlistar_backlog(RN-164), обслуживаемые двумя внутренними маршрутами с областью ПРОЕКТА — и область здесь ключевая: чтение по сессии было ровно тем, что скрывало ранее захваченное правило. Они переиспользуют уже существовавшую трубопроводку (listByTypeForProject,ListBacklogUseCase,computeCoverage); не хватало маршрута, метода в клиенте и инструмента. - Ничто не требовало историю после эпика, и цикл заканчивался молча.
Эпик без истории стал ЯВНЫМ исходом —
backlog.epic_without_story, долговечным, по образцу RN-059 (RN-165), — а предел итерацийPoServerначал выдаватьtoolloop.limit_reached, ТОТ ЖЕ тип, что уToolLoop, который разговорные агенты никогда не выдавали, имея собственный цикл (RN-166). - Инструкция kickoff не говорила ни слова о том, что делать, когда
не хватает информации, — а перед пробелом без инструкции модель выбирает
между выдумыванием и остановкой. Остановка — то, что она сделала. Третий выход стал
записан, с инструментом для его применения:
ask_structured_questions, ТОТ ЖЕ, что у Criativo (RN-162), теперь объявленный и PO.
Два решения замысла, важные за пределами этого случая: только create_story, который
удался, гасит обязательство эпика (история, отклонённая api, ничего
не покрыла, и обращение с ней так, будто покрыла, — это замена одного молчания другим); а
требование — по СЛУЧАЮ, сообщается один раз, никогда не тревога, повторяющаяся каждый
ход, пока кто-нибудь не научится её игнорировать.
РАУНД exp001 — нить читается так, как происходит ход (RN-172/173)
То же происхождение, что у других: ИСПОЛЬЗОВАНИЕ, а не сценарий. Четыре жалобы на экран Сессии — handoff PO появлялся НАД его последней репликой, одобрение поднималось в середину ответа Arquiteto и опускалось только когда он заканчивал, карточка одобрения «плохо свёрстана», и прокрутка не следовала за разговором.
Расследование выяснило, что упорядочивание было ВЕРНЫМ, и в этом урок
раунда. RN-155 верна event log; а лог говорит, что
po_server.ex#run_turn/2 выдаёт в ТОЙ ЖЕ итерации agent.response, ЗАТЕМ
tool.call для offer_handoff и ТОЛЬКО ПОТОМ рекурсирует к заключительной реплике
— значит, seq handoff честно МЕНЬШЕ, чем у последней реплики. То же
для proposed_action.created. Исправление, следовательно, не в упорядочивании: это
объявленное правило ПРЕДСТАВЛЕНИЯ (RN-172), в отдельном проходе
(afundarDesfechos) ПОСЛЕ sort по seq — а не компаратор из трёх
членов, который никто не смог бы обосновать через год.
То, что не даёт ходам смешиваться, — три барьера, и каждый существует
из-за реального случая: ход (у границы между двумя ходами может не быть
ВИДИМОГО элемента — agent.activated открывает ход и не становится элементом нити),
автор (в сессии выполнения несколько агентов пишут, ни разу не дав пользователю
сказать, и все остаются в одном ходе) и не-исход (два исхода
подряд сохраняют порядок между собой — Infra перед Dev Lead).
RN-173 закрывает две другие жалобы, и они были одной и той же: эффект
прокрутки зависел только от [events.length, streamingText], а actions —
ОТДЕЛЬНЫЙ запрос — пришедшая карточка выталкивала разговор с экрана, ничего не
прокручивая. Зависимость исправлена, плюс ResizeObserver над содержимым
для того, до чего НИ ОДИН список зависимостей не дотягивается (сворачивание Disclosure,
перекомпоновка Markdown). Защита в 120px от конца остаётся нетронутой,
намеренно: нить следует за разговором, а не захватывает чтение того, кто прокрутил вверх.
РАУНД exp001 — нить говорит, кто говорит, с какой моделью и что спрашивает (RN-171/174/175/176)
То же происхождение: ИСПОЛЬЗОВАНИЕ. Четыре жалобы на экран Сессии, и три из них разделяют один урок — продукт знал информацию и не показывал её.
Вопрос по центру, и с выходом (RN-171). Жалоба была буквальной: «sempre
dê a opção de input do usuário quando ele seleciona Escreva». Модель
предлагала вариант вроде «Напишите сами», а в форме негде было
писать — схема ask_structured_questions не умела выразить «помимо
этих, что угодно». allowOther вошёл в select со значением по умолчанию
true, и асимметрия — аргумент: список, закрытый по ЗАБЫВЧИВОСТИ, стопорит
разговор, и пользователь не может разблокировать его извне; список, открытый по ошибке, предлагает
лишнее поле. Интерфейсный sentinel никогда не попадает в backend, а кнопка
отправки по-прежнему требует всего заполненного (backend в любом случае отклоняет с 400).
Блок также стал центрированным с пределом 560px от
ApprovalCard и получил аватар: это был единственный элемент нити, ни к чему не выровненный.
Действие, запускающее ход, взводит индикатор (RN-174). Анимация «думает»
уже существовала — не хватало ПОКРЫТИЯ. Два действия экрана запускают
синхронный ход в engine и ничего не включали: ответ на форму
(AnswerStructuredQuestionUseCase переиспользует SendAgentMessageUseCase) и возврат
истории PO (ReturnStoryUseCase вызывает reviseStory, то есть
handle_call({:revise, …})). Канал Phoenix не закрывает эту дыру: пока join
ещё идёт (RN-108), у agent.status «working» нет слушателя, и он теряется.
Модель — для ВСЕХ (RN-175). Предпосылка, что дефект был у PO, не
подтвердилась: четыре разговорных агента записывают modelName начиная с RN-146, с
зелёным тестом. Никогда не записывал ToolLoop — путь каждого агента
выполнения и gate — и чат без активного агента в api. Ни одного нового
вызова: RunLlmTurnUseCase уже возвращал поле в теле. Экран перестал
писать одинокое слово «modelo» (которое читается так, будто модель так и называется)
и стал читаемым chip-ом; без данных он ГОВОРИТ, что они не были записаны, —
угадывание по текущему binding приписало бы старому ответу модель, которой,
возможно, даже не существовало, — та же ошибка, которую предотвращает замороженная цена из RN-044.
Таблица в Markdown становится таблицей (RN-176). Было возможно два выхода, и
выбран поддерживать таблицу в Markdown чата, с Table дизайн-системы.
Отображение artifact.module_map в нити было ОТКЛОНЕНО по уже
зафиксированному решению: это ДЕЙСТВУЮЩЕЕ состояние проекта, а не артефакт, датированный
сессией (RN-159), и он живёт в Обзоре. Кроме того, запрос касался
таблицы внутри сообщения — и исправление служит любому агенту, который
её напишет, а не только Карте модулей. Разделительная строка остаётся
обязательной (GFM), поэтому проза с | никогда не становится таблицей по ошибке.
РАУНД exp001 — панель группирует, упорядочивает и говорит, чего не показывает (RN-177..181)
То же происхождение, что у других: ИСПОЛЬЗОВАНИЕ, а не сценарий. Пять жалоб на панель контекста и лог Сессии, и урок раунда один — панель показывала срез так, будто это целое, в трёх разных измерениях одновременно: скрывала шесть типов событий, не предлагая альтернативы, читала от самого старого к самому новому и обрезала сессию на 200 событиях, не говоря об этом.
Источник как новая классификация (RN-177). ActivityKind отвечает «о каком
ПРЕДМЕТЕ говорит событие» и определяет иконку и цвет; origem отвечает «из какого СЛОЯ
оно пришло», и именно он делает историю читаемой — eventos, sistema, llm,
harness, agente, usuario, выводимые из СУЩЕСТВУЮЩИХ данных (actor.kind и
префикс type). Приоритет — то, что делает её предсказуемой, и он задан порядком
if: механизм побеждает актора (tool.call принадлежит harness, кто бы ни был), а
актор побеждает префикс агента (chat.message существует с обеих сторон). 5 самых
свежих остаются открытыми, а остальное сворачивается по источнику, в обоих местах —
в панели лога и в нити, где ось инвертирована (нить идёт по возрастанию, поэтому
история оказывается ВВЕРХУ). Фильтр машинного шума по-прежнему включён по
умолчанию: его причина не изменилась (116 из 193 реальных событий), изменилось то, что он
стал ВЫБОРОМ, а не свершившимся фактом.
Порядок и пагинация (RN-178). Четыре раздела стали убывающими, и кнопка «Carregar mais antigos» как следствие сменила сторону — прежний аргумент с обратным знаком. Бизнес-правила свыше 5 пагинируются, причём страница разрешается через clamp, а не эффектом синхронизации.
Дерево PO (RN-179). backlog.task_created никогда не попадал в панель;
теперь эпик → история → задача образуют дерево по СВЯЗИ, которую событие уже
несёт (epicId/storyId), никогда по соседству в логе — узел без загруженного родителя
поднимается в корень, а не подвешивается наугад.
Предел, который стал видимым (RN-180), — самый ценный пункт. Панель
читала 200 событий через prop и не могла сказать, что их больше. Она стала читать
пагинированную историю RN-099 (тот же queryKey, что у хвоста, НОЛЬ лишних запросов
— RN-090/091), получила pager, который у ActivityFeed был всегда, но ни одно место
вызова не передавало, а заметка считает, сколько не хватает, ВЫЧИТАНИЕМ по seq, как
колокольчик из RN-100. Производные разделы читают baixados (всё уже пришедшее), а не
окно ленты, иначе стали бы показывать МЕНЬШЕ, чем раньше.
Делегирование в нити (RN-181). delegation.completed|failed|dispensed
существовали только в панели лога: gate открывался и закрывался без какого-либо знака того, что
снизу была вторая попытка. Они стали компактным уведомлением в формате RN-157,
с фразой из classifyEvent — той же, что в панели. Внешний контракт
области не меняется (ADR 0038): нить рассказывает то, что записал lead, и не начинает
адресовать субагента.
ПРОГРАММА 28 — Волна 1, фронт A: тема перестаёт быть недостижимой (RN-182..185)
Светлая тема существовала в design/tokens.css, имела тест паритета и была
описана в design/README.md так, будто это экран, — и ничто в apps/web
не писало data-theme. Единственный способ её увидеть — вписать атрибут в
DevTools. Оба теста контраста знали и говорили об этом: один утверждал через expect,
что ТРИ пары светлой темы проваливаются, — «запись того, что наследуется, а не гарантия того, что
отображается». Пока это длилось, аргумент был честным — измерять экран, который
никто не может открыть, значит измерять намерение. С приходом кнопки темы он
меняет знак.
Boot — это ФАЙЛ (apps/web/public/theme-boot.js), синхронный в <head>,
до бандла. Handoff велит использовать INLINE-скрипт; образ раздаётся под
script-src 'self' (ADR 0058), поэтому inline работал бы в pnpm dev:web и
блокировался бы в production — сбой ADR 0036 с другим подлежащим. tema.ts
— API, которое потребляет shell; кнопка в нём не живёт. localStorage, который выбрасывает исключение,
деградирует до «рисует и не сохраняет», а неизвестное значение падает на значение по умолчанию.
Шесть токенов светлой темы изменили значение (ADR 0074), потому что самый
требовательный фон светлой темы — бумажный --code-bg: accent 3,56 → 4,81, warning
3,15 → 4,98, success 3,89 → 5,12, violet 4,16 → 4,95, text-muted 2,76 → 5,17,
плюс accent-hover на ступень ниже. В ТЁМНОЙ не изменился ни один, и её долг
остаётся закреплённым на тех же пяти числах. Два зафиксированных побочных эффекта:
пять пар, являющихся долгом в тёмной, ПРОХОДЯТ в светлой, а «известное исключение
дизайн-системы» (--on-accent поверх --accent, 3,20:1) исчезает в светлой —
исправление, которое описывал комментарий теста, затемнение до --terracota-500,
— как раз то, что стала делать светлая.
Пять из восьми значений --syn-* из handoff были ОТКЛОНЕНЫ измерением (
худшее, --syn-cm, даёт 2,32:1 в светлой). Где handoff проваливается, действует измеренное
число: это та же линейка, что и для шрифтов. Восемь ролей начинают существовать с префиксом
--syntax-*; пять семантических, которые ещё раскрашивают, измеряются рядом с ними.
ПРОГРАММА 28 — Волна 1, фронт D0: расходы по provider-у (RN-186..188)
Сдерживание сменило природу, и важно именно это — а не новая ось.
provider снова стал измерением sumGroupedBy, пересматривая ADR 0063 по
решению владельца продукта. Раньше участник не дотягивался до разбивки по
УЧЁТНЫМ ДАННЫМ, потому что её не существовало, — ADR 0063 написал «нет аргумента,
который можно передать». Теперь она существует, и сдерживает ТИП: две перегрузки, и область, которая
несёт actor (единственная в представлении участника), принимает только
Exclude<SpendDimension, 'provider'>. Гарантия перешла от «невозможно
выразить» к «невозможно скомпилировать», что слабее, — и именно поэтому
барьеров ДВА, и они независимы: у маршрута участника тоже нет параметра
измерения. Ни одного if по комбинации, намеренно (тот же урок, что у
RN-153/154), а доказательство — ИНВЕРСИЕЙ: без @ts-expect-error tsc указывает
TS2769 на точной строке.
porOwner/porAgente — разбиение porAtor по actor_kind, без лишнего
запроса; system остаётся вне обоих и по-прежнему входит в итог. Индекс по
token_usage(created_at), который ADR 0063 измерил и отложил, вошёл в миграцию 0044.
ЭКРАН не изменился — это более поздняя волна.
ПРОГРАММА 28 — Волна 2, фронт H2: рейтинг моделей без выдуманной оценки (RN-210, ADR 0077)
Handoff просил двух вещей, зависевших от ОДНИХ И ТЕХ ЖЕ данных, которых у продукта
нет: оценку качества по способности (mock: «код → claude-sonnet-4 /
qwen2.5-coder:14b (9.4)») и бейдж «ideal» в ModelPicker, когда
модель покрывает «все способности, требуемые агентом». Расследование
дважды подтвердило, что этих вторых данных не существует: сам
ModelsSection уже переименовал столбец «Agente · capacidades» просто в
«Agente» по этой причине, а ModelPicker binding-а агента читает
GET /projects/:id/models (роль viewer) — курирование (uses, ADR 0051)
существует только в ModelComCuradoria, из маршрута только для maintainer. Бейдж НЕ
был построен; таблица «Melhores modelos por capacidade» БЫЛА, с двумя
реальными сигналами: сколько агентов проекта уже разрешаются через каскад в
каждую курированную модель, и стоимость каталога как тай-брейк — никогда
вычисленная оценка.
ПРОГРАММА 28 — Волна 2, фронт B: боковая панель сворачивается (RN-195..201)
264px/62px с дорожкой иконок при свёртывании; раскрываемые проекты, показывающие
вкладки (читаемые из project-tabs.ts, никогда не захардкоженные); раздел Активность, сгруппированный
по базовому агенту/НАСТОЯЩЕМУ экземпляру — суффикс -2 из extraDevAgentId, никогда
выдуманный счётчик -01/-02 (находка Волны 1/фронта B0), — с областью
проекта текущего маршрута, а не всех, чтобы не открыть N+1 по событиям. Кнопка
темы в подвале; два инертных глобальных пункта уходят, только Проекты и Активность
глобальны; автосвёртывание вкладки Código через AutoCollapseContext
(sidebar-state.ts), без записи предпочтения. Сохранение централизовано в
sidebar-state.ts, шесть ключей brabo.* из handoff.
Два расхождения с handoff задокументированы и НЕ решены (RN-197): бейдж
проекта остаётся pendingApprovalsCount (RN-151 побеждает, она свежее), а
цвет идентичности из handoff появляется только на свёрнутой дорожке — развёрнутая
строка сохраняет только существующую точку статуса (RN-039), две точки были бы шумом.
ПРОГРАММА 28 — Волна 2, фронт C: рамка экрана (RN-202/203, ADR 0078)
Четыре буквальных исправления из чек-листа «moldura de tela» handoff-а:
.headerTop получил min-height: var(--header-h) — ПОЛ, а не потолок, потому что
заголовок проекта (идентичность + компактный TokenMeter) богаче
общего заголовка 6 внутренних экранов handoff-а, и принудительные 60px обрезали бы
предупреждение о бюджете — тот же класс ошибки, который запрещает RN-088; активная вкладка с
box-shadow: inset 0 -2px 0 var(--accent) вместо border-bottom; горизонтальная
прокрутка в линейке; контейнер содержимого с max-width: 1040px во вкладках
документального вида (не в semRespiro). Значения отступов линейки,
жившие как CSS-override потомка в ProjectPage.module.css
с ФАЗЫ 16, переехали в Tabs.module.css, теперь с тем же владельцем.
Часть, требовавшая решения, а не только CSS: handoff перечисляет 7 вкладок проекта, а
реестр (project-tabs.ts) — 10. executores (RN-121), backlog
(RN-048) и insights (гипотезы Psicólogo) родились ПОСЛЕ handoff-а, с
реальными данными и собственной RN, и ОСТАЮТСЯ — handoff — эталон ВИЗУАЛЬНОЙ точности,
а не потолок того, сколько вкладок у продукта (RN-203, ADR 0078). А ключ
sessions по-прежнему подписан «Chat», никогда «Chat RAG», как просит более
свежий handoff (RN-202): «Chat RAG» описывал бы функциональность, которой не
существует — контракт embeddings готов (ADR 0075), но ничто его ещё не
потребляет, без конвейера индексации и без UI цитирования, — и переименование вкладки
сегодня было бы той же ошибкой, которую ADR 0042 уже отвергает для модели каталога:
объявлять способность до того, как она существует. Ни одна ФОРМА экспорта не изменилась
(AbaDoProjeto, ABAS_DO_PROJETO); только метка code сменилась с «Code» на
«Código».
ПРОГРАММА 28 — Волна 2, фронт H1: видимый фокус (ADR 0036)
Select, Modal (кнопка закрытия) и ProjectCard получили :focus-visible
с той же откалиброванной обработкой, что в Input.module.css, включая блок
forced-colors: ни у одного из трёх не было индикации фокуса, достижимой только с
клавиатуры. Кнопка закрытия Modal выросла с 30px до 32px, минимальной цели
касания на десктопе. Table и Badge прошли аудит и НЕ потребовали
изменений — ни один из двух не открывает собственной интерактивной возможности: строка
Table — чистое представление (тот, кому нужна кликабельная строка, уже использует
<button>/<a> внутри ячейки через render), а Badge нигде в продукте
не используется с onClick.
ПРОГРАММА 28 — Волна 3, фронт H3: вкладка Criativo получает KPI, не изображая несуществующих данных (RN-227..230)
Handoff просит 4 значка статуса для 5 реальных состояний сессии
(created/active/closing/closed/closed_abnormally). closed_abnormally→
прервана и created→ожидает — прямые; closing НЕ был слит с
«закрыта» — он получает собственный значок («encerrando», тон accent, пульсирующий),
потому что в closing исход ещё неизвестен, и назвать её «закрытой»
было бы ложью об этом. Pill-фильтры (только 4, по той же причине) группируют
created и closing по ТРАЕКТОРИИ, а не по внешнему виду: created попадает в
«Ativas» (ещё в игре), closing — в «Fechadas» (на пути к закрытию
без ошибки) — значок строки фильтр никогда не переписывает, меняется только группировка.
Из 4 KPI два честны отсутствием данных: «стоимость месяца»
ПЕРЕИСПОЛЬЗУЕТ getMySpend (тот же queryKey, что у вкладки Gastos, представление
участника/RN-101) и никогда не показывает итог проекта — это данные владельца
(porProjeto), а вкладку Criativo видит любой участник; «доля идеация
→ commit» ОБЪЯВЛЕНА отсутствующей (показывает «—»), потому что в продукте сегодня нет связи между
сессией и commit — выдумывать расчёт было бы тем же классом
ошибки, который ADR 0042 уже отвергает для оценки модели.
ПРОГРАММА 28 — Волна 4, фронт G2: Chat RAG получает конвейер индексации и гибридный поиск (RN-231..238, ADR 0080)
Поверх таблицы chunks из Волны 3 (ADR 0079): РУЧНАЯ индексация (без
watcher, POST .../rag/reindex, идемпотентная полная пересборка — удаляет
область/сессию перед воссозданием) трёх честных областей — docs/adr
через ReadProjectCodeUseCase (те же учётные данные/шлюз/проверка пути,
что у вкладки Code) и session только из chat.message/agent.response. Чанкинг
по заголовку/абзацу, 1200 символов с перекрытием 150 —
задокументирован как отправная точка, а не калибровка по реальным данным (пока
не существует корпуса вопросов, прогнанного по индексу). Гибридный поиск —
это ДВА независимых запроса (вектор через HNSW, лексический через GIN),
слитых взвешенной суммой (0.6/0.4) и обрезанных по порогу (0.2) —
числа тоже отправная точка. Когда provider embedding (фиксированный:
ollama) не отвечает, конвейер записывает чанки БЕЗ вектора и объявляет
пробел в отчёте — никогда не изображает полную индексацию, а поиск деградирует
до чисто лексического с предупреждением. Экран (Chat RAG как отдельная вкладка) — Волна
5; этот фронт — только backend.
ПРОГРАММА 28 — Волна 4, фронт F1: жизненный цикл контейнера становится таблицей, без оркестратора (RN-243..248, ADR 0081)
См. пункт 31 ФАЗЫ 25 (ЧАСТИЧНО ОТМЕНЁН): project_containers
(миграция 0046) записывает изменяемое СОСТОЯНИЕ — provisioning → running ⇄ stopped, failed достижим из всех трёх, из removed выходят только повторным provisioning
— отличное от artifact.project_image (неизменяемого РЕШЕНИЯ
Arquiteto). Подтверждено расследованием (grep, ноль вхождений):
НИ ОДИН сервис продукта не монтирует /var/run/docker.sock и не работает
privileged — оба варианта использования только ЗАПИСЫВАЮТ и ЧИТАЮТ, ни одного настоящего
вызова Docker. Первый переход требует уже решённого образа (RN-105)
и ЗАМОРАЖИВАЕТ версию/ресурсы в строке; проект local (ADR 0072)
отклоняется, потому что не поднимает собственный контейнер. Предел ресурсов
(cpus/memory_mb/pids_limit) ОБЪЯВЛЕН, не применён — ни один
экран не утверждает «контейнер ограничен X», только «зарегистрированным намерением
было X». Ни одного нового HTTP-маршрута в то время: без реального потребителя открывать
его значило бы угадывать контракт — закрыто Волной 5/фронтом F2 чуть ниже.
ПРОГРАММА 28 — Волна 5, фронт G3: Chat RAG получает собственную вкладку (RN-252..254, ADR 0082)
Экран, который RN-202 (Волна 2/C) отложила — «Chat RAG» описывал бы
способность, которой ещё не было, — приходит вместе с backend-ом Волны
4/G2: key: 'rag', НОВАЯ вкладка (ordem: 28, между Código и Backlog),
НИКОГДА не переименовывающая sessions. Различие осталось тем же, изменилась только причина:
sessions — разговор с АКТИВИРОВАННЫМ агентом (тратит ключ владельца
за ход, RN-058); rag — поиск по уже построенному индексу, без
какого-либо агента посередине, только чтение по природе. Экран потребляет три
контракта ADR 0080, не угадывая форму, и показывает две честные
деградации, которые backend уже объявлял и ни один экран не читал, — vectorAvailable: false становится видимым предупреждением, а покрытие никогда не выдумывает «переиндексировано
X мин назад». Цитата с источником session ведёт к точному СОБЫТИЮ, переиспользуя
механизм, который уже используют chip-ы улик Psicólogo; источник file
показывает путь/заголовок как текст, без ссылки — у вкладки Código сегодня нет
deep-link по пути, это вне объёма здесь.
ПРОГРАММА 28 — Волна 5, фронт F2: настоящий потребитель жизненного цикла контейнера (не терминал) (RN-267/268, ADR 0083)
Исходным планом этого фронта был полноценный интерактивный терминал.
Расследование подтвердило, что ФАЗА 25b остаётся отсечённой: ни один сервис
не монтирует /var/run/docker.sock, и даже после ADR 0081 ничто в
production не переводит project_containers — не существует настоящего контейнера,
чтобы открыть терминал ВНУТРИ него. Реализация притворного терминала или
терминала, работающего в том же контейнере, что и монорепозиторий Brabo (долг, который ADR 0055
уже описывает как политику, а не изоляцию), выдумала бы способность —
та же ошибка, которую ADR 0041/0042 отвергают для provider-а LLM и модели
каталога. Настоящая сдача: GET /projects/:projectId/container/lifecycle
(role:viewer), первое HTTP-представление жизненного цикла (пересматривает ADR
0081, который отложил маршрут из-за отсутствия потребителя), и вкладка Terminal,
показывающая это состояние — переведённый бейдж, причину сбоя — ПОД
уже существовавшим поясняющим текстом, никогда вместо него, запрашиваемое только пока
вкладка открыта. null (ни разу не подготовлен) — честный и
самый частый сегодня результат.
ПРОГРАММА 28 — Волна 5, фронт I: социальный вход отменяет запрет из backlog ADR 0031 (RN-272..283, ADR 0084)
Явный запрос владельца продукта, осознающего последствия для
безопасности — запрет снимается только для GitHub/GitLab; MFA, OIDC provider и
общая федерация остаются вне объёма (см. «O que NÃO fazer»).
SocialLoginCallbackUseCase заканчивается в ТОМ ЖЕ EmitirSessaoUseCase, что и
вход по паролю, — никакого второго формата сессии. Переиспользует существующий
клиент OAuth, но у state СОБСТВЕННОЕ назначение
(purpose: 'social_login', проверяется РАНЬШЕ любого другого поля):
он никогда не принимает state потока ПОДКЛЮЧЕНИЯ git к проекту, что
было бы эскалацией привилегий. Новая таблица social_identities (миграция
0047), ключ (provider, provider_user_id) — никогда e-mail/логин.
Решение в три шага: известная идентичность → вход; e-mail совпадает с
существующей учётной записью И подтверждён provider-ом → связывает (и подтверждает
e-mail учётной записи, если он ещё не был); e-mail совпадает, но НЕ подтверждён →
отказ — набранный e-mail не доказывает владения, принятие открыло бы
account takeover; без соответствующей учётной записи → создаёт новую учётную запись без
пароля, в том же состоянии «pendente», что и при миграции из Keycloak. Переиспользует ТОТ ЖЕ
OAuth app подключения git — ноль новых переменных окружения, только
разные redirect_uri/scope на поток; действие ОПЕРАТОРА по
регистрации второго callback у provider-а — то, что оправдывает ветку
breaking/.
Аудит fluxo.yml × код — план Dev Lead становится настоящим одобрением (RN-284, ADR 0086)
Это не запланированная фаза: она закрывает расхождение, которое нашёл аудит только для чтения
docs/fluxo.yml × код (находка A2,
docs/explanation/auditoria-fluxo-vs-codigo.md) — поток уже объявлял
выход plano-de-paralelismo у dev-lead как via: proposed_action
начиная с ADR 0085, а код так и не был приведён в соответствие. Владелец
продукта решил, что ошибался код.
propose_execution_plan вошёл в decide.ts (минимальная роль
maintainer), НАМЕРЕННО вне блока абсолютных пределов — его можно
настроить на автоодобрение, в отличие от
parallelize/raise_max_parallel. Dev Lead — ПЕРВЫЙ разговорный
агент (все выполняют синхронный ход через GenServer.call до
180s), который приостанавливается в ожидании человеческого решения — шаблон уже существовал для
dev agent (ADR 0052) и gate-ов QA/Infra (ADR 0057), но оба
запускаются через cast и никогда не имели дела с ожидающим синхронным from.
TurnoAssincrono.tratar_resultado/2 получил ветку: при наличии
ключа :aguardando_aprovacao (проверяемого по ЗНАЧЕНИЮ, а не по наличию —
Dev Lead несёт его nil начиная с init/1) отвечает на from так же
и в то же время, что всегда, но выдаёт только agent.status: awaiting_approval вместо agent.done — ход не закончился.
LiveBroadcast.agent_status/4 получил новый статус в guard. Возобновление
использует ТОТ ЖЕ Engine.Dev.Wake, который QaLeadServer уже переиспользует для
субагентов QA (доставка — по АГЕНТУ, а не по типу).
Принятый, объявленный пробел: без собственной таблицы состояния перезапуск
engine, пока Dev Lead приостановлен, теряет подписку — решение
остаётся записанным и видимым в Одобрениях, но он не рассказывает исход
сам. Закрытие этого потребовало бы того же механизма сохранения, что у ADR
0052; вне объёма этого исправления, которое лишь приводит поведение к тому, что
docs/fluxo.yml уже объявлял.
UX Designer — пятый разговорный агент, досрочно (RN-285..287, ADR 0087)
Это не сработавший триггер разделения — docs/fluxo.yml всегда объявлял
«когда у УПРАВЛЯЕМОГО проекта будет собственный интерфейс для проектирования»
критерием, и он не сработал: дизайн-система остаётся статичным входом.
ОСОЗНАННОЕ решение владельца продукта всё равно ввести роль досрочно.
Engine.Agents.UxDesignerServer отражает DevLeadServer — GenServer на
сессию, предел 14 итераций, активируется handoff-ом accepted, адресованным
«ux-designer», через уже существующий ОБЩИЙ механизм (ни одна строка не изменилась в
ActivateAgentUseCase). SOLO: без области, без субагентов. Kickoff читает
самый свежий artifact.product_brief — ту же «бизнес-потребность»,
которую производит Criativo, без нового артефакта, — а дизайн-система
(design/tokens.css, design/COMPONENTS.md) ОПИСАНА в идентичности
агента (Engine.Harness.Agents), статичный текст: у разговорных агентов
нет инструмента чтения файлов репозитория.
propose_prototype — ЕДИНСТВЕННЫЙ инструмент (personas, jornadas,
prototipo с telas/anotacoes, resumo). Записывает
artifact.prototipo_navegavel без выделенного варианта использования в api —
в отличие от choose_project_image/create_c4_diagram, которым он нужен,
потому что у них есть содержимое, ВЫВОДИМОЕ из другого артефакта, или общий доменный
отказ; ни одна из этих причин здесь не действует, поэтому проверка формы
живёт в engine (ArtifactSchemas), а запись использует общий
append_event_returning, который api уже открывает, — тот же путь, что artifact.product_brief.
Один артефакт, два handoff-а по нему — для «po» и для «dev-lead», — никогда
второй артефакт для «spec-visual»: прототип (экраны + аннотации) И ЕСТЬ
визуальная спецификация, а его дублирование рисковало бы расхождением двух копий потом. Переиспользует
половину замысла ADR 0086, которая выживает без его приостановки:
успешный propose_prototype завершает ход, чтобы модель не предлагала
снова и не дублировала артефакт, — без «aguardando_aprovacao», потому что предложение
прототипа не имеет внешнего эффекта.
apps/web/src/lib/agents.ts получил запись (color: var(--accent),
наименее переиспользуемый семантический токен roster; icon: PencilIcon).
uxDesignerActive вошёл в roster в ОБОИХ источниках, которые RN-090 требует
синхронизировать, — панели команды (agent-status.ts) и карточке dashboard
(projects-summary.repository.ts, тот же запрос, что infraActive,
расширенный, без нового query).
Вне досягаемости, объявлено: teste-de-usabilidade требует настоящего
пользователя-человека — ни один агент это не заменит. metricas-de-uso остаётся пробелом даже
при analytics active (ADR 0089): отчёт о воронке измеряет
сессия→commit→PR→merge, а не принятие функций КОНЕЧНЫМИ пользователями
проекта, который строит Brabo, — «улики принятия по функции»
ОБЪЯВЛЕНЫ метрикой без пути к существованию сегодня, а не незакрытым пунктом,
который закроют в следующем раунде.
Staff — код готов, спит для автоматического запуска (RN-305/306, ADR 0088)
Это не запланированная фаза: docs/fluxo.yml объявляет Staff/Principal Engineer
как status: planned начиная с ADR 0085 («контракт готов, активация
решена, ждёт триггера»). ОСОЗНАННОЕ решение владельца продукта:
написать КОД заранее, зная, что АВТОМАТИЧЕСКИЙ триггер (Anamnese,
замечающая ПОВТОРЯЮЩУЮСЯ системную проблему) не сработает — Anamnese
на паузе (ANAMNESE_ENABLED=false, решение от 2026-08-10). Спит
для автоматического запуска, но не для РУЧНОГО вызова.
Шестой solo-разговорный агент (Engine.Agents.StaffServer), рядом с
Criativo/PO/Arquiteto/Dev Lead/UX Designer, отражающий Arquiteto (ограниченный цикл
с пределом 14) с двумя отличиями: БЕЗ kickoff/1 (нет артефакта
сессии для резюмирования — поднимается и простаивает до первого user_message,
которым тот, кто адресовал handoff, объясняет проблему) и активируется
ОБЩИМ путём canActivateAgent (handoff accepted, адресованный
«staff»), не входя в USER_STARTED_AGENTS — расследование подтвердило, что
никаких доменных изменений в api не требовалось, потому что
assertHandoffTargetAllowed отклоняет только субагента области, а у Staff нет
области. Единственный инструмент, propose_rfc (проблема, варианты с компромиссами,
рекомендация, одноразовый PoC), записывает artifact.rfc_staff НАПРЯМУЮ через
append_event_returning — тот же бестабличный шаблон, что у emit_insight, а
не у artifact.c4_diagram (который выводит уровень Container из
module_map в api), — и возвращает handoff Arquiteto в ТОМ ЖЕ tool call,
без proposed_action (регистрация документа — не внешний эффект).
staffActive вошёл в roster панели команды и в карточку dashboard
(ProjectCardSummary.roster), по тому же критерию, что infraActive, чтобы не
открыть расхождение, о котором уже предупреждал комментарий RosterFacts.
Объявлено, не скрыто: SessionPage.tsx/AGENTES_DE_CHAT не
тронуты — тот же уже принятый шаблон, что и для infra (НАСТОЯЩИЙ и АКТИВНЫЙ lead
тоже вне этого списка). Сквозной путь использования сегодня — внутренний
маршрут (POST .../agent/message, agent: "staff"), а не экран
Сессии; общий UI «ручного handoff выбранному агенту» остаётся в
backlog.
Активный gate implementavel — QA-стратегия как второй момент qa-lead (ADR 0090)
Осознанное решение владельца продукта досрочно ввести триггер, который
docs/fluxo.yml уже предусматривал («когда gate implementavel активируется»), —
триггер ЗДЕСЬ — сама работа по построению механизма, а не симптом
использования, ждущий своего часа. docs/gates.yml, implementavel:
status: planned → active (владелец dev-lead, severidade: warn
не тронута — реестр уже говорил «рождается warn, даже когда активируется»).
QA-стратегия перестаёт быть ролью proposto: это САМ qa-lead во
втором МОМЕНТЕ — тот же процесс, результат отдельный от вердикта PR,
ровно как docs/fluxo.yml уже объявлял («может быть сам qa-lead
во втором МОМЕНТЕ, не обязательно новый агент: разделение — по
результату»). Engine.Gates.QaLeadServer.run_design/3 — НОВАЯ и ДОБАВОЧНАЯ точка
входа (без касания run/2, привычного пути, привязанного к
DevAgentState.find_by_task_id), вызываемая через
Engine.Gates.Dispatcher.run_qa_estrategia/3 (та же заменяемая в
тесте косвенность, что уже используют run_qa/2/run_secops/2).
Engine.Gates.QaEstrategiaAgent — модуль БЕЗ СОСТОЯНИЯ (не GenServer),
той же формы, что QaPerformanceSegurancaAgent, — реестр без Terminal
(ReadFile, SearchWorkspace, EmitPlanoDeTeste), — но КОНТЕКСТ
другой: Engine.Gates.QaEstrategiaContext.fetch/3 получает ТОЛЬКО story (из
list_backlog) и действующий module_map (из get_infra_context, переиспользуемого только
ради поля moduleMap — ноль новых маршрутов), без dev_state и
worktree_path, потому что gate работает PRE-DEV: ещё нет ни dev agent, ни worktree,
ни task_id. Ни один из трёх инструментов не проходит через
ActionPipeline (проходят только terminal/write_file), поэтому этот агент
НИКОГДА не приостанавливается — run_design/3 выполняется синхронно внутри собственного
handle_cast. Предел итераций остаётся 8 (разговорный), а не 60
(gate) — НАМЕРЕННО: без token_budget_micros под ним (нет task,
PRE-DEV) — та же причина, по которой infra-workflows остаётся на 8, даже используя
инструмент (RN-085). Результат (emit_plano_de_teste: синтез,
исполнимые критерии, ОБЩАЯ стратегия автоматизации без фреймворка)
становится artifact.plano_de_teste в event log вызвавшей сессии.
assess_implementability, новый инструмент Dev Lead, читает
самый свежий artifact.plano_de_teste story в истории СОБСТВЕННОЙ
сессии: если плана ещё нет, запускает оценку и возвращает ошибку с просьбой
повторить (ошибка инструмента — ВХОД цикла, а не конец пути —
RN-163); при наличии плана предлагает заключение (implementavel/inviavel +
обоснование, план встроен в payload) как proposed_action, по ТОМУ ЖЕ
шаблону трёх исходов, что propose_execution_plan (ADR 0086) —
maintainer, НАМЕРЕННО вне блока абсолютных пределов
decide.ts.
appsec перестаёт быть proposto — второй момент SecOps входит в
следующем разделе.
appsec получает второй момент secops (RN-360/361, ADR 0090)
docs/fluxo.yml (id: appsec, camada_seguranca) заранее объявлял
«тот же шаблон, что у QA: два МОМЕНТА, а не два агента пока» — осознанное
решение владельца продукта ввести активацию досрочно, не дожидаясь gate
implementavel (фронт qa-estrategia, тот же концептуальный ADR 0090), который
сам реестр называл триггером.
Engine.Gates.SecOpsAgentServer.run_design/2 выполняется в ТОМ ЖЕ процессе, что
secops PR (тот же ключ Registry), без каких-либо Diff/Scanner/
DevAgentState: получает story из backlog + действующий module_map
(Engine.Gates.AppSecContextBuilder, без dev_state/worktree_path) и
вызывает Engine.Gates.AppSecAgent.run/3 — модуль БЕЗ СОСТОЯНИЯ (не GenServer,
той же формы, что QaPerformanceSegurancaAgent), реестр инструментов БЕЗ
Terminal, выполняющий чек-лист STRIDE-lite через ToolLoop.run/1 над
ПРОЕКТОМ story, никогда над кодом. Заканчивает выдачей artifact.threat_model
и созданием handoff для трёх объявленных lead-ов (arquiteto, dev-lead,
infra — АДРЕСУЕМЫЙ агент id area-infra, RN-361).
Объявленный пробел, а не баг: run_design/2 ВЫЗЫВАЕМ, но ничто пока не вызывает его
само — естественный триггер — assess_implementability от Dev Lead
(фронт qa-estrategia), вне объёма этой сдачи, которая была сохранена
самодостаточной (decide.ts/docs/gates.yml/dev_lead_tools.ex не тронуты).
analytics/delivery-metricas становятся отчётом (RN-320..322, ADR 0089)
Осознанное решение владельца продукта ДОСРОЧНО ввести две роли
целевой модели (docs/fluxo.yml, status: proposto), не дожидаясь
органического триггера, который каждая уже объявляла. Форма — та, которую сам поток
предписывал: analytics «поглощён medicao», пока метрика ПРОДУКТА не
станет обязательным входом PO, delivery-metricas «никогда не становится агентом —
становится ОТЧЁТОМ medicao». Обе становятся одним СКРИПТОМ —
apps/api/scripts/analise-funil.ts (pnpm --filter api analise:funil -- --projeto <uuid> [--json]), в ТОМ ЖЕ формате, что medir-execucao.ts
(Фаза 13b): чистое чтение через Drizzle, ноль записи, без GenServer, без
агента LLM.
Измеряет ПО-НАСТОЯЩЕМУ, по proposed_actions.execution_result трёх git-действий
dev agent (git_commit/pr_open/git_merge, только status: 'executed'): настоящую воронку (сколько сессий дали commit / открытый PR /
смерженный PR, и конверсию между этапами — считает СЕССИИ, а не действия), настоящий lead
time (от первого commit до первого merge сессии, по updated_at
ВЫПОЛНЕНИЯ, а не предложения) и настоящий deployment frequency (merge в
ветку PROTECTED_BRANCHES, по дням, со ссылочной сверкой с gate
backmerge из docs/gates.yml — чья улика — CI, вне досягаемости
скрипта, который только читает базу).
Три метрики остаются в разделе «Não medido, de propósito» — ПОСТОЯННОЕ
объявление, а не пробел, закрываемый в следующем раунде: полная продуктовая воронка
идеация → commit (sessions без storyId, RN-230, потребовала бы
новой схемы — ни одной миграции в этом фронте); улики принятия по
функции (это не недостающие данные для сбора — Brabo не инструментирует
проекты, которые он СТРОИТ, и нет никакого пути к существованию этой телеметрии
сегодня); MTTR и change failure rate (требуют сигнала настоящего ИНЦИДЕНТА
production, та же зависимость, что у secops-runtime/platform —
другой фронт).
secops-runtime как скрипт-отчёт (RN-375..377, ADR 0091)
Решённое владельцем продукта досрочное введение роли secops-runtime
(docs/fluxo.yml, camada_seguranca, ранее proposto), не дожидаясь
объявленного триггера («production с реальным трафиком, после DEPLOY_ENABLED +
активного platform»). Входит только то, чего триггер НЕ требует: pnpm --filter api relatorio:seguranca-runtime читает rate_limit_hits — данные, которые
RateLimitGuard из ADR 0027 уже записывает, в том числе при трафике dev/CI, — и
производит рейтинг корзин (user:<uuid>/ip:<endereço>, два единственных
записываемых формата; без маршрута и причины, которых таблица никогда не хранила) и
временное распределение по фиксированным срезам. Это СКРИПТ, а не агент LLM и не
GenServer: по данным нечего решать, только агрегировать.
Автоматическое обнаружение инцидентов, реагирование на инцидент и постмортем
безопасности остаются ВНЕ — они зависят от того же несработавшего триггера, — и
отчёт перечисляет их в ПОСТОЯННОМ разделе «não medido», не симулируя
примерный инцидент и не выдумывая числа (тот же принцип, что у ADR
0041/0042/0077). Хранимое окно коротко по замыслу
(DomainGaugesCollector.pruneRateLimit удаляет hits старше
2 × RATE_LIMIT_WINDOW_MS, 240s по умолчанию), и отчёт объявляет оба
окна — НАСТРОЕННОЕ и НАБЛЮДАЕМОЕ, — никогда не давая второму выдать себя за
историю длиннее, чем она есть.
platform получает первую сдачу: отчёт телеметрии по требованию (RN-385/386, ADR 0092)
Осознанное решение владельца продукта досрочно ввести часть роли platform
(docs/fluxo.yml, camada_plataforma) — чей status остаётся planned и
чья ativacao остаётся синхронизированной с DEPLOY_ENABLED, которого не существует.
pnpm --filter api relatorio:telemetria [--projeto <uuid>] [--json] — это
СКРИПТ (не агент LLM, не GenServer), который читает по требованию ТЕ ЖЕ
вопросы, которые DomainGaugesCollector уже собирает для scrape
Prometheus, — активные/closing сессии по проекту, заблокированные tasks по
проекту, состояние последнего бэкапа (всегда ГЛОБАЛЬНОЕ, никогда по проекту, потому что
продукт делает бэкап всей базы), — и завершается после печати.
SQL-запросы ПОВТОРЕНЫ, а не импортированы из коллектора: его методы
приватны и заканчиваются записью в gauge Prometheus, без чистой половины «только
запрос», которую можно было бы переиспользовать, не привязывая CLI-скрипт к жизненному циклу
@Injectable.
Вывод всегда содержит «где посмотреть больше» (версионируемые dashboards в
deploy/k8s/observability/dashboards/*.json, алерты в
deploy/k8s/observability/alerts/brabo-alerts.yaml, docs/runbook.md #observabilidade, pnpm dev:obs для локальной наблюдаемости) — ссылка, никогда
дублирование — и раздел «não medido», который ОБЪЯВЛЯЕТ, не выдумывая: ни один числовой
SLO в продукте не определён, постмортем зависит от реального инцидента, которого
не было, а телеметрия, возвращающаяся в продукт по замкнутому циклу, — то, что
сделало бы platform active, — триггер (DEPLOY_ENABLED) по-прежнему отсутствует.
gate_saida: { id: operavel, status: planned } роли не меняется;
docs/fluxo.yml лишь получает nota в выходе telemetria-consolidada,
говорящую, что ручная/по требованию версия уже реальна.
dbre становится двумя механическими скриптами (RN-400..403, ADR 0093)
Из роли dbre в fluxo.yml только два из четырёх результатов зависят от
реального объёма данных (plano-de-capacidade, tuning — остаются пробелом,
без срока). Двум другим реальная нагрузка не нужна: parecer-de- migracao — распознавание шаблонов в SQL-тексте
(apps/api/scripts/lint-migracao.ts, обходит apps/api/src/db/migrations/ *.sql, сигнализирует DROP TABLE/TRUNCATE/DROP COLUMN/ALTER COLUMN ... TYPE/ADD COLUMN ... NOT NULL без DEFAULT, != 0, если что-то найдено, — сегодня
находит 3 вхождения в уже смерженных и принятых миграциях, информативно, не
исправлены попутно); backup-restore-testado уже был реальным с
Фазы 5 (CronJob + backup_runs + выполненный make test-restore, RTO
~40s) — не хватало лишь отчёта по требованию (relatorio:backup, то же
чтение, что у DomainGaugesCollector.collectBackup()). Ни один из двух
пока не входит в CI: линтер обходит весь репозиторий, а не diff
PR, и стал бы gate-ом, проваливающим PR из-за находки, которая ему не принадлежит, — см.
ADR 0093 о технике области (pr-police.ts), когда это изменится.
Правило «одна миграция на волну» (meta/_journal.json) перестало
называться «механизированной версией» этой роли: оно предотвращает конфликт
снимков между параллельными агентами — забота, ортогональная «в этом SQL
есть рискованный шаблон».
Аудит fluxo.yml × код — Волна 1: fluxo.yml/gates.yml в актуальном состоянии
Первая из шести волн плана аудита (раздел D,
docs/explanation/auditoria-fluxo-vs-codigo.md), которая закрылась, — Волны 3, 4 и
5 уже были введены досрочно вне порядка (analytics/DORA, gate
implementavel, QA-стратегия/appsec); эта — только метаданные, без ADR, без
строки production-кода.
docs/gates.yml (paralelismo-autorizado): status: planned → active,
с evidencia в ТОМ ЖЕ формате, что у соседнего gate implementavel —
event_types: [proposed_action.created/approved/denied], filtro: { actionType: parallelize }, onde: request-parallelization.use-case.ts (находка A1/B5 — fluxo.yml уже
говорил ativo начиная с ADR 0053/ФАЗЫ 14d, отстал только gates.yml).
docs/fluxo.yml получил ещё три исправления аудита, все — цитирования
или метки, ни одного поведенческого: машина состояний dev выросла
с 4 до 5 состояний (вошёл awaiting_approval, Фаза 12e/ADR 0052 — A3);
цитата RN-160 ушла из входа backlog-promovido Arquiteto и переехала
туда, где она реально применяется, — на выход handoff-duplo (кнопку
«Confirmar arquitetura pronta», которую она блокирует, — A4); цитата RN-161
ушла из handoff-duplo и стала заметкой во входе Dev Lead (именно шаг
ПРИНЯТИЯ handoff цепляет активацию выполнения, а не сам двойной handoff
— A5); и происхождение worktree-por-agente у dev перестало говорить
harness (Engine.Dev.WorktreeManager не является одним из 4 компонентов
роли harness — A8). docs/explanation/backlog.md тоже был
обновлён: строка об устаревшем gates.yml ушла из таблицы
незакрытых пунктов (закрыта здесь), вместе с gate implementavel (B3, уже
закрытым ADR 0090 до этой волны, но никем не убранным из таблицы)
и частью B7 (deployment frequency/lead time), которую analise:funil
(ADR 0089) уже сдаёт по-настоящему — MTTR/change failure rate остаются
вне, но как ПОСТОЯННЫЙ пробел, уже объявленный в fluxo.yml, а не
инженерный незакрытый пункт.
Проверка: pnpm --filter api validacao:gates -- --sem-banco (реестр
действителен, без ativo-sem-evidencia) и pnpm docs:check зелёные. Фаза 3
скрипта (реальные улики в event log) требует --projeto <uuid> против
базы с данными — вне досягаемости этого исправления, которое касается только метаданных.
Незакрытые пункты того же плана, не этой волны: Волна 2 (делегирование Dev Lead →
dev, RN-160, повторно проверяемая в backend) и Волна 6 (gate necessidade-validada,
который требует ADR и продуктового решения до написания кода).
Аудит fluxo.yml × код — Волна 2: RN-160 в backend и делегирование Dev Lead → dev (RN-404/405, ADR 0094)
Вторая из шести волн плана аудита (раздел D), ДВА пункта
наибольшего объёма после Волны 1 — остальные четыре (3, 4, 5 введены досрочно вне
порядка; 6 всё ещё не закрыта, требует ADR и продуктового решения). Оба живут
исключительно в apps/api; никаких изменений в engine/web не
потребовалось.
B6 — RN-160 повторно проверяется в backend. Правило («Confirmar arquitetura
pronta» требует хотя бы 1 продвинутой истории) гарантировалось только на КЛИЕНТЕ
(SessionPage.tsx, hasPromotedStory) — прямой HTTP-вызов
POST /agents/arquiteto/handoff-infra полностью игнорировал правило.
OfferInfraHandoffUseCase.execute теперь запрашивает
StoryRepository.findByProject и отказывает с BadRequestException ДО
записи события или вызова engine, когда ни у одной истории нет
status !== 'draft'.
B1 — делегирование Dev Lead → dev становится данными. ADR 0053 (пункт 5) уже
предусматривал это и объявил вне объёма в то время — CLAUDE.md перечислял это как
обратимое отсечение. Закрыто здесь: AcceptParallelizationUseCase.execute
(сторона API, даром покрывает прямой И одобренный пути, потому что оба уже
сходятся в нём) записывает delegations с area: 'dev', leadAgent: 'dev-lead', subagent = id только что активированного dev-<modulo>. В отличие
от QA/Infra (которые записывают на стороне ENGINE, потому что там субагент производит
заключение), status: 'completed' был ПЕРЕОПРЕДЕЛЁН для этой области: он означает
«делегирование ОСУЩЕСТВЛЕНО» (агент поднялся), а не «заключение выдано», — а
parecerArtifactId указывает на самый свежий artifact.module_map
проекта (через SessionEventRepository.listByTypeForProject, уже
существующий метод, без нового запроса). Без module_map в проекте — чего не должно
случаться — делегирование не записывается с фальшивым id: лишь логирует
неожиданное состояние и продолжает, потому что активация dev agent уже успешна, когда эта
запись предпринимается; сбой в RecordDelegationUseCase тоже не роняет
активацию, по той же причине. Решение зафиксировано в ADR 0094, который также
отменяет отсечение пункта 5 ADR 0053.
docs/fluxo.yml (dev-lead, выход delegacao): status: lacuna →
status: ativo, regra: RN-405.
Аудит fluxo.yml × код — Волна 6 (последняя): gate necessidade-validada (RN-406, ADR 0095)
Закрывает ПОСЛЕДНЮЮ из шести волн плана аудита (раздел D,
docs/explanation/auditoria-fluxo-vs-codigo.md) — закрылись все шесть: 3,
4 и 5 были введены досрочно вне порядка (analytics/DORA, gate
implementavel, QA-стратегия/appsec, ADR 0089/0090); 1 и 2
закрылись в предыдущих PR (fluxo.yml/gates.yml в актуальном состоянии; RN-160 в
backend + делегирование Dev Lead → dev). Это 6-я: gate
necessidade-validada (Criativo → PO), объявленный proposto начиная с ADR
0085 без какого-либо механизма за ним.
modelo-de-time.md уже фиксировал, почему он стоял: Criativo (
модель), в одиночку решающий, что потребность, которую он сам произвёл,
подтверждена, — это самоподтверждение, а не настоящий gate. Решение владельца
продукта — ОТДЕЛЬНОЕ человеческое подтверждение — третья кнопка в
SessionPage.tsx, «Confirmar necessidade validada», по ТОМУ ЖЕ шаблону, что
«Confirmar arquitetura pronta» (RN-160): выделенный маршрут
(POST .../agents/criativo/validate-necessity), выделенный вариант использования
(ValidateNecessityUseCase), собственное событие (necessity.validated).
Никогда не переиспользовал confirm_readiness/RN-142 (который остаётся лишь
структурным полом, «≥1 захваченное правило»), ни принятие handoff со стороны PO
(AcceptHandoffUseCase, структурное, без суждения о содержании).
Выбранная цепочка: кнопка включается только ПОСЛЕ того, как confirm_readiness
уже консолидировал artifact.product_brief (hasProductBrief) — нет
смысла «подтверждать» резюме, которого ещё нет, и это самое
согласованное с docs/fluxo.yml прочтение (gate — ВЫХОДНОЙ для Criativo,
момент, когда его работа уже сдала конкретный артефакт).
В отличие от OfferInfraHandoffUseCase, это подтверждение НЕ сигнализирует
engine — handoff Criativo→PO уже произошёл внутри самого
confirm_readiness, поэтому новое событие — лишь запись ПО СУЩЕСТВУ, без
какого-либо триггера, ждущего его.
docs/gates.yml получает gate necessidade-validada: active,
aprovacao_humana: true, но severidade: warn — ничто в продукте сегодня
не СПРАШИВАЕТ о прохождении этого gate, прежде чем позволить PO продолжить, в отличие от
block у story-promovida/plano-de-adocao, за которыми стоит настоящая
блокировка в коде. docs/fluxo.yml (criativo, gate_saida): status: proposto → status: ativo, regra: RN-406.
Аудит fluxo.yml × код — B4 (ПОСЛЕДНИЙ незакрытый пункт): PO читает метрики продукта (RN-407)
Закрывает пункт B4 — последнюю строку таблицы «Backlog do modelo de time
(ADR 0085)» в docs/explanation/backlog.md; таблица становится ПУСТОЙ после
этого исправления, и аудит fluxo.yml × код здесь заканчивается. Без нового
ADR: следует тому же шаблону, уже установленному RN-164 (чтение агента
с областью проекта, без внешнего эффекта, без proposed_action), только с
новой RN. docs/fluxo.yml (роль po, вход metricas-de-produto):
status: lacuna → regra: RN-407.
ДАННЫЕ существовали начиная с ADR 0089 (analise:funil — настоящая воронка
сессия → commit → PR → merge, настоящий lead time, настоящий deployment frequency);
не хватало лишь того, чтобы PO мог ПРОЧИТАТЬ этот отчёт внутри хода. Третий
инструмент чтения PO, listar_metricas_de_produto
(GET .../product-metrics), в точности того же замысла, что два родственных из
RN-164, — :direct, без параметра, область закрыта на проекте. Чистые функции
РАСЧЁТА и запрос (calcularFunil/calcularLeadTimes/
leadTimeMedioMs/deploymentFrequencyPorDia/buscarAcoesGitDoFunil)
переехали из apps/api/scripts/analise-funil.ts в
apps/api/src/application/services/funil-metrics.ts — МЕХАНИЧЕСКИЙ и необходимый
рефакторинг (scripts/ не может жить в src/, а src/ не
импортирует из scripts/): скрипт стал РЕЭКСПОРТИРОВАТЬ оттуда, не меняя
сигнатуры и поведения, а регрессионный тест скрипта остаётся
зелёным, не будучи тронутым. В JSON-теле нет поля для трёх постоянных
отсутствий, которые скрипт уже объявлял только текстом (полная продуктовая воронка
идеация → commit, улики принятия по функции, MTTR/change
failure rate), — инструмент PO всегда называет их по имени в ТЕКСТЕ, который возвращает
модели, чтобы она никогда не заключила по отсутствию чисел, что
пробела нет.
GOVERNANCE.md — критерий режима community, а не его механизм
Пункт прежнего backlog («Modo community do approval-ladder») стал исправлением
записи, а не реализацией: расследование подтвердило, что режим community
уже существует в коде начиная с ФАЗЫ 6 (scripts/ci/approval-ladder.ts), протестирован
с обеих сторон и лишь выключен через APPROVAL_MODE=solo (по умолчанию). Фраза
backlog («становится изменением aprovacao_humana в реестре gate-ов») пришла из
СПЕКУЛЯТИВНОЙ строки ADR 0054 — не отредактированной, по правилу, но неточность
зафиксирована в docs/explanation/backlog.md: aprovacao_humana у gate
aprovacoes-da-escada уже фиксированно true в обоих режимах, без какого-либо поля в
схеме Gate, реагирующего на APPROVAL_MODE.
На самом деле не хватало другого TODO(humano), в
docs/explanation/branching-policy.md: критерия того, кто входит в каждый
список одобряющих (devs/po/gestão) — кто входит, кто выходит, на основании
чего. GOVERNANCE.md (корень репозитория, новый) закрывает это: МЕХАНИЗМ остаётся
в branching-policy.md (лестница, разные люди, пошаговая
смена переменной), КРИТЕРИЙ теперь живёт в GOVERNANCE.md. Это предложение,
а не работающий комитет — у проекта по-прежнему один мейнтейнер, и
документ говорит об этом прямо: большая его часть описывает то, что начнёт
действовать, КОГДА людей станет больше, а не уже активную структуру. Новое правило warn
в docmap (governanca) требует пересмотра, когда меняются approval-ladder.ts/
branching-policy.md, — отдельно от существующего правила block
(politica-de-branches) по той же причине, по которой checks-e-rulesets отделено
от него: МЕХАНИЗМ вытекает из кода, КРИТЕРИЙ — человеческое решение.
Настоящий SMTP в MailSender (RN-408, ADR 0096)
Закрывает пункт backlog, открытый с отказа от Keycloak (ADR 0032, «настоящий SMTP
остаётся будущей конфигурацией»). nodemailer поверх чистого SMTP-транспорта
(СТРОЧНЫЙ протокол с состоянием/MIME/STARTTLS/AUTH, в отличие от JSON HTTP API,
с которыми интегрируется остальной продукт), за MAIL_TRANSPORT — log
(по умолчанию, В ТОМ ЧИСЛЕ в production: отправка настоящей почты — opt-in
оператора) или smtp, выбираемый через useFactory в AuthUseCasesModule,
не трогая ни одного варианта использования (RegisterUseCase,
RequestPasswordResetUseCase, LoginUseCase, скрипт
migrate-keycloak-users.ts по-прежнему лишь внедряют MailSender).
SMTP_HOST/SMTP_USER/SMTP_PASSWORD/SMTP_FROM следуют шаблону RN-114
в production, когда режим smtp, — но БЕЗ публичного значения по умолчанию, которое есть у
четырёх исходных RN-114, поэтому линейка вступает в действие, только когда оператор выбрал
smtp; проверка выполняется в конструкторе SmtpMailSender, задействованном
через useFactory при сборке графа provider-ов (тот же замысел, что у
CREDENTIALS_MASTER_KEY), а не немедленным вызовом в main.ts. Тело —
ЧИСТЫЙ ТЕКСТ, никогда HTML: порт не несёт структуры для богатого тела, а
шаблонизатор был бы поверхностью инъекции без какого-либо выигрыша.
Расследование нашло настоящий пробел до написания кода: у email_verification не было
никакого web-маршрута — verificarEmail существовал в
apps/web/src/lib/auth.ts с Фазы 7a без ЕДИНОГО вызывающего, так что
ссылка из письма подтверждения вела бы в никуда, как только SMTP
начал бы отправлять настоящую почту. /verificar-email
(VerifyEmailPage.tsx) закрывает это, отражая SetPasswordPage.tsx — тот же
шаблон validateSearch в router.tsx, тот же единый ответ для несуществующей/
истёкшей/уже использованной ссылки — с одним отличием: без формы
подтверждение срабатывает само при монтировании, поэтому экрану нужны три
состояния RN-088 (загрузка/ошибка/успех), а не только два.
«N агентов онлайн» в dashboard — ЖИВОЙ статус, никогда историческое присутствие (RN-409, ADR 0097)
Пункт прежнего backlog закрыт. Предварительное расследование подтвердило, что
не существовало никакого агрегата liveness, ни по проекту, ни по workspace — только
ИСТОРИЧЕСКОЕ присутствие (RosterFacts, «уже хоть раз появлялся»), а настоящий живой
статус — только на клиенте, выводимый из event log, только когда проект
ОТКРЫТ. ProjectCardSummary.onlineAgentCount суммирует два механизма по
ОДНОЙ линейке («не простаивает, не застрял»): dev agents
(engine.dev_agent_states.status NOT IN ('idle', 'idle_tripped'),
пакетная агрегация в engine) и разговорных агентов (последний agent.status
самой свежей сессии с status !== 'idle', payload, принимающий только
working/idle/awaiting_approval). QA/SecOps никогда не считаются — они не выдают
agent.status, единственный вердикт на вызов, без понятия «простоя» между
вызовами.
Архитектурное решение, которое стоит записать: запрос читает
engine.dev_agent_states НАПРЯМУЮ, сырым SQL, пакетированным по workspace, внутри
ТОГО ЖЕ Promise.all, который уже суммирует одиннадцать запросов read model RN-090
(ДВЕНАДЦАТЬ → ЧЕТЫРНАДЦАТЬ, по-прежнему КОНСТАНТА), — вместо того чтобы открывать новый внутренний
HTTP-маршрут в engine. api и engine разделяют ОДНУ физическую базу,
разделённые лишь схемой Postgres, и прецедент уже был
(apps/api/scripts/medir-execucao.ts уже читает engine.oban_peers); это
решение поднимает шаблон с ручного скрипта до ПРОТЕСТИРОВАННОГО production-кода
(минимальный и объявленный fixture таблицы engine в
test/support/global-setup.ts). Принятое и объявленное в ADR
0097 следствие: запрос предполагает, что тот, кто эксплуатирует продукт, мигрирует обе стороны
вместе (db:migrate И engine:migrate), и падает громко/видимо, если мигрировала только api
— без try/catch, скрывающего ошибку.
Сессия выполнения больше не закрывается при ожидающем dev agent (RN-411)
Исправление дефекта, найденного в РЕАЛЬНОЙ сессии выполнения, а не пункт
сценария: пять dev agents поднялись, стали idle_tripped (RN-047,
circuit breaker), застряв в ожидании, пока пользователь разблокирует задачу за
задачей, а 30-секундный heartbeat сессии (Engine.Sessions.SessionServer)
закрыл сессию из-под них, пока ручная разблокировка продолжалась.
Третий сигнал GetSessionPendingWorkUseCase (RN-064) читает только
agent.status, словарь разговорных агентов, — dev agents
говорят dev.* (Engine.Dev.AgentIo) и никогда не выдают agent.status, поэтому
ни один dev agent никогда не удерживал сессию этим сигналом. Четвёртый сигнал
читает последнее событие dev.* каждого dev-<modulo>/dev-<modulo>-2 в
сессии: dev.working/dev.blocked/dev.idle_tripped удерживают сессию
(застрявший в ожидании человеческого решения — ЭТО незакрытая работа); только dev.idle
(по-настоящему дренированный) не удерживает. dev.awaiting_gate/dev.awaiting_approval
ВОШЛИ в линейку через RN-412 — см. ниже; остаточный пробел, который описывал этот
абзац, закрыт.
Автоматический личный workspace при регистрации (RN-410)
Найдено при навигации: кнопка «Novo projeto» в dashboard ничего не делала, без
ошибки. Корневая причина: RegisterUseCase и SocialLoginCallbackUseCase (в ветке,
которая создаёт новую учётную запись) создавали пользователя и учётные данные, но НИКОГДА
workspace — КАЖДАЯ новая регистрация упиралась в эту стену, и это не проявлялось раньше
только потому, что seed.ts всегда создаёт workspace вместе с демонстрационными данными.
Обе точки теперь создают workspace и добавляют пользователя как owner
в ТОЙ ЖЕ транзакции, которая уже создаёт учётную запись. Имя/slug получаются из единственной
чистой функции nomeESlugDoWorkspacePessoal (domain/auth/personal-workspace.ts),
чтобы правило не расходилось в двух файлах; slug всегда получает суффикс
userId.slice(0, 8) (тот же шаблон, что extraDevAgentId), чтобы быть уникальным
без обращения к базе (RN-410).
413 в PR и Executores «без выполнения» — оба конца и цепочка (RN-412, ADR 0098)
Это не запланированная фаза: исправление, запрошенное реальным ИСПОЛЬЗОВАНИЕМ («nas PRs sempre está
estourando entity too large»), плюс родственный дефект вкладки Executores,
появляющейся пустой при реально идущей работе. Расследование нашло их
сцепленными: gate умирает с 413 → dev-ы застревают в
dev.awaiting_gate → это состояние не удерживало сессию (пробел
RN-411) → 30-секундный heartbeat закрывает сессию из-под них → вкладка требует
активной сессии и исчезает со всем roster.
Причина 413 была в API Brabo, а не в provider-е LLM —
комментарии, приписывавшие ошибку provider-у (runtime.exs,
terminal_executor.ex, docker-compose.yml), были неверны;
FalhaDeTurno.origem({413, _}) уже классифицировал её как "codigo", и
был прав. apps/api/src/main.ts никогда не настраивал предел тела
Express — действовало значение по умолчанию 100 KB, намного ниже 8 MB, которые
принимает Phoenix, в самом тяжёлом направлении транспорта (engine → api,
POST /internal/sessions/:sessionId/llm-turn, который пересылает ВСЮ
историю на каждой итерации ToolLoop). API_JSON_BODY_LIMIT (по умолчанию
10 MB) закрывает транспортный конец.
Второй конец, без которого поднятие лимита лишь отложило бы переполнение:
ContextManager engine (ADR 0098) имел ДВА дефекта, которые
делали компактизацию недостижимой до переполнения тела —
estimate/1 считал только content, поэтому сообщения assistant,
полные toolCalls, стоили в оценке ~ноль токенов; а окно
компактизации использовало только context_window (128.000 токенов у агентов
gate), что давало ~350 KB до компактизации — далеко после
любого разумного транспортного предела. ЭФФЕКТИВНОЕ окно теперь
min(context_window, teto_de_transporte), с обрезкой всегда на
ГРАНИЦЕ ИТЕРАЦИИ ToolLoop (никогда не отделяя assistant с
toolCalls от role: "tool", которые на него отвечают, — это сломало бы протокол
tool-use provider-а).
RN-412 расширяет RN-411: DEV_PENDING_TYPES получил
dev.awaiting_gate (исходный аргумент — gate может умереть и бессрочно
держать dev agent в этом состоянии), а также
dev.awaiting_approval, по новому аргументу, найденному в расследовании:
решение об одобрении записывает proposed_actions.status
СИНХРОННО, а возобновление dev agent через outbox/Oban — АСИНХРОННО;
в окне между ними ничто не удерживало сессию — тот же дефект
RN-411 уровнем глубже.
Вкладка Executores/Обзор имели родственный дефект на стороне web:
executionActivated выводился из events.some(...) по окну
всего из 200 событий useSessionEvents — execution.activated — одно из
ПЕРВЫХ событий сессии и выпадало из окна в любом реальном
выполнении, стирая весь roster. Правильное значение уже существовало, агрегированное
по ВСЕМ событиям, в сводке workspace (RN-090,
projects-summary.repository.ts) — оба экрана стали потреблять
это значение. Бонусная находка по пути: ExecutionSection (Обзор)
пересчитывал activated снова, независимо, по тому же
окну — длительное выполнение снова предлагало «Ativar execução»,
будто никогда не начиналось; исправлено заодно. gatesEverOpened
страдает от ТОГО ЖЕ класса дефекта и остался ОБЪЯВЛЕННЫМ известным
ограничением, не исправленным здесь — его исправление потребовало бы изменить сигнатуру
deriveAgentRoster/rosterFactsFromEvents, вне объёма этого
точечного исправления.
Neo4j как граф знаний — фундамент (RN-413/414/415, ADR 0099/0100)
Запрос владельца продукта: Neo4j + ollama-model-loader (gemma:1b,
yi-coder:1.5b, nomic-embed-text), явно вдохновлённый репозиторием
ErickWendel/neo4j-ai-experiments
— благодарность зафиксирована в ADR 0099 и в prompts/README.md, за
конкретное вдохновение шаблоном «промпт как версионируемый файл, а не строка,
зашитая в код» и графом как памятью агентов.
Две ответственности, один фундамент: версионируемые шаблоны промптов
(PromptTemplate/PromptVersion, идемпотентно по хешу — RN-413) и
реляционная память (взаимодействия, гипотезы Psicólogo с уликами,
профили Anamnese, handoffs — реальное потребление — будущая волна, эта сдача
лишь строит механизм). Граф — ПРОИЗВОДНАЯ память, никогда не источник
истины; pgvector остаётся векторным индексом чанков, без
дублирования embedding в двух базах. Драйвер neo4j-driver в api (не в
engine) — api уже владеет всей персистентностью и RAG, engine
потребляет по внутреннему HTTP с service token, как всегда.
rag_search (RN-414) закрывает самую большую дыру существующего RAG: ни один агент его не
запрашивал, только web-вкладка «Chat RAG». Новый инструмент в engine, собственные пределы
(RN-150), деградация всегда видна в начале текста для модели.
ollama-model-loader (RN-415) закрывает отдельный настоящий баг: nomic-embed-text
никогда не скачивался автоматически, и RAG молча деградировал до чисто лексического
в любой чистой среде.
Первая партия шаблонов вынесена в prompts/*.md (ux-designer,
Psicólogo, Anamnese, суммаризация ContextManager).
Потребление графа — шаблоны без касания GenServers, релевантность без замены свежести (RN-416/417, ADR 0101)
InstructionFiles получил источник :graph (db > graph > dir > root —
instruction_patch пользователя по-прежнему побеждает всё), и ТРИ агента
стали ПЫТАТЬСЯ разрешить шаблон из графа перед inline-текстом, который
стал обязательным fallback-ом: ux-designer (graph_instruction_templates_enabled?),
Psicólogo и Anamnese (graph_templates_enabled?, один ключ на
обоих). Два флага, а не один — они столкнулись бы ПРОТИВОПОЛОЖНЫМИ значениями по умолчанию между
параллельными фронтами, если бы делили ключ; оба значения по умолчанию — false.
Только context-manager-summarize (из четырёх шаблонов партии) остался без
потребителя в то время — пробел, который ADR 0101 объявил открытым и который закрылся
позже, через ЧЕТВЁРТОГО потребителя ТОГО ЖЕ graph_templates_enabled?:
промпт суммаризации ContextManager (prompt/1, см. RN-417).
Psychologist.ContextBuilder/Anamnese.ContextBuilder получили
ВТОРОЙ источник контекста — rag_search, с запросом, выведенным из триггера
анализа, — в КОМПОЗИЦИИ с привычным чтением свежих/временного окна,
никогда не в замену: без совпадения поведение идентично прежнему.
Фрагменты входят в СУЩЕСТВУЮЩИЙ бюджет Triage, и
degraded: true от RAG всегда видим в контексте, никогда не скрыт.
Граф пишет себя сам: GraphProjector (api) дренирует ВТОРУЮ
строку outbox (aggregateType: 'graph_projection', та же транзакция,
значение, которое фильтр engine — IN ('session', 'task') — никогда не совпадает,
избегая гонки с Engine.Outbox.Drain, который уже дренирует 'session'
за ~2s), и вызывает уже существующие варианты использования записи. Engine
НИКОГДА не пишет в граф напрямую — это сохраняет event log единственным источником
истины; граф восстанавливается воспроизведением.
Настоящая находка во время проверки, записанная, потому что учит: тест
предела тела RN-412 (apps/api/test/main.spec.ts) имел СКРЫТЫЙ
баг — AppModule, импортированный СТАТИЧЕСКИ в начале файла, вычисляет
DrizzleModule (который создаёт свой пул подключений в области МОДУЛЯ, а не
в фабрике Nest) ДО того, как beforeAll успевает направить
DATABASE_URL на тестовую базу. Это маскировалось, пока локальная база dev
тоже была мигрирована (та же таблица, поэтому запрос «находил», даже
указывая не туда); вскрылось, как только база dev была
пересоздана пустой (настоящим smoke-тестом этой же волны против
Neo4j+Postgres). Исправлено ДИНАМИЧЕСКИМ await import(...) внутри
beforeAll, после того как DATABASE_URL уже задан, — это шаблон, которому следовать
всегда, когда тесту нужно перенаправить DATABASE_URL для настоящего
AppModule.
Локальный runner — выполнение на машине пользователя (RN-418/419/420, ADR 0102/0103)
Запрос владельца продукта: агент, работающий на МАШИНЕ ПОЛЬЗОВАТЕЛЯ, в локальной папке проекта, через стандартный терминал пользователя, — плюс настоящий интерактивный терминал во вкладке Code. Два изменения, одно зависит от другого.
Политика (ADR 0102 — ГЛОБАЛЬНОЕ решение, явно подтверждённое владельцем
продукта после автоматического предупреждения безопасности об изменении):
git push/PR/deploy (RN-106) и sudo/doas (новые) выходят из безусловного
deny и становятся АБСОЛЮТНЫМ ПРЕДЕЛОМ — безусловным require_approval,
в ТОМ ЖЕ блоке и по ТОМУ ЖЕ шаблону, что соседние пределы (current.policy === 'auto_approve'), никогда не автоодобряемым ни через agent_autonomy (подстановка "*"),
ни через permissions.json. Щель, которую исходный deny затыкал силой,
закрыта в ИСТОЧНИКЕ: ApproveAlwaysActionUseCase/patternForAction
отказываются записывать шаблон в allow для действия с внешним git-эффектом или
привилегированной командой — «sempre permitir» одобряет только экземпляр, никогда
шаблон, для этих двух случаев. Без этой второй половины предел был бы
декоративным. sudo/doas совпадают по ГЛАГОЛУ в comandoPrivilegiadoNoComando
(external-effect.ts), обходя все сегменты команды, — тот же
принцип, что у efeitoExternoNoComando.
Runner (apps/runner, новый workspace Node/TS, ADR 0103): CLI
(brabo-runner --project <id> --dir <pasta> --token brb_...), который
пользователь запускает на своей машине, подключается к ENGINE через НОВЫЙ канал Phoenix
(/runner,
топик terminal:<projectId>), аутентифицированный ОДНОРАЗОВЫМ тикетом (тот же
шаблон RN-108, но тикет ВЫПУСКАЕТСЯ ENGINE — схема "engine",
собственная миграция — и запрашивается api через внутренний HTTP-маршрут, инвертируя
поток тикета сессии). Выполняет exec (УЖЕ ОДОБРЕННУЮ команду агента
— маршрутизация в TerminalExecutor происходит только ПОСЛЕ привычного конвейера
decide()/proposed_action, никогда до) в $SHELL
пользователя, и интерактивный режим PTY для вкладки Terminal (pty_open/pty_data/
pty_input/pty_resize/pty_close — engine делает чистый RELAY, никогда
не интерпретируя байты). Только ОДИН runner на проект (Engine.Runners.Registry,
:global, исключительность :global.register_name/3); режим runner БЕЗ
проверенного workspace или БЕЗ подключённого runner-а явно ОТКАЗЫВАЕТ — никогда
не падает в System.cmd/bind-mount, которых для проекта runner не существует
(пересмотрено ADR 0104/RN-423, см. выше; откат на привычный
путь по-прежнему действует, но только для container/mounted). PTY — действие
аутентифицированного ПОЛЬЗОВАТЕЛЯ, не проходит через proposed_action, но аудируется
(terminal.session.started/ended в event log, в том числе когда вкладка
падает без явного pty_close).
Граница безопасности runner-а — НЕ sandboxing, это аутентификация
(CLI предъявляет Personal Access Token УЧЁТНОЙ ЗАПИСИ пользователя) + привычный
конвейер одобрения + согласие пользователя запускать
CLI на собственной машине, с его привилегиями. apps/runner/src/guard.ts
проверяет cwd внутри корня лексическим разрешением, но объявлен
BEST-EFFORT, а не настоящей гарантией.
PAT закрыл пробел токена автоматизации (ADR 0105/RN-424..426).
Настоящая находка Волны 1 — «у продукта нет механизма ДОЛГОЖИВУЩЕГО токена учётной записи
для автоматизации, поэтому runner повторяет вход браузера
(cookie/CSRF, сохранённые в ~/.brabo/runner-credentials.json)» — была
закрыта в Волне 2: personal_access_tokens (brb_…, хеш HMAC-SHA256+
pepper через hashDeToken(), никогда argon2 — неверно для секрета ВЫСОКОЙ
энтропии) выпускается в Настройках проекта, с областью ОДНОГО проекта,
отзываемый, с опциональным истечением. apps/runner/src/auth.ts полностью потерял
интерактивный вход, cookies и файл учётных данных — он лишь
проверяет формат и передаёт --token/BRABO_ACCOUNT_TOKEN, НИКОГДА не записываемый
CLI на диск. PAT никогда не аутентифицирует вне
POST .../runner-ticket, по ПОСТРОЕНИЮ (IS_PAT_ROUTE_KEY/
@RequirePatAuth() + PatAuthGuard) — никогда ветка в глобальном JwtAuthGuard,
которая позволила бы RolesGuard авторизовать PAT для любого маршрута
роли пользователя.
maintainer отзывает PAT другого пользователя (RN-427), закрывая пункт,
который ADR 0105 пока объявил вне объёма (реагирование на
инцидент — уволенный dev с утекающим токеном). Расширение той же модели,
без нового ADR: ОТДЕЛЬНЫЕ маршруты (GET .../personal-access-tokens/all,
DELETE .../personal-access-tokens/:tokenId/admin), область по
projectId вместо userId — самоотзыв каждого пользователя
(RN-426) не меняется.
@xterm/xterm/@xterm/addon-fit (web) и phoenix/node-pty (runner) —
четыре новые зависимости — та же линейка исключения, что у mermaid
(ADR 0068): динамический import(), отсутствие eval/new Function подтверждено
grep-ом по установленному пакету (это не формальная гарантия против обфускации, только
сильная улика — объявлено как неопределённость, а не утверждено как уверенность).
ADR 0104 — Волна 1 ЗАВЕРШЕНА (RN-421/422/423): ADR 0072 и 0103
никогда не разговаривали друг с другом — RN-170 требовал bind-mount при создании, а маршрутизация в
runner (RN-420) переиспользовала тот же флаг workspace_mode == 'local' без
какого-либо bind-mount, поэтому настоящее использование runner-а по-прежнему было обязано
проходить проверку смонтированной папки. workspace_mode (2 значения) стал
execution_mode (container/mounted/runner, миграция 0048) —
RN-170 (теперь RN-422) начинает действовать только для mounted; runner рождается с
путём, проверенным только ЛЕКСИЧЕСКИ (без I/O), и workspaceVerifiedAt: null,
продвигаемым в проверенный, когда runner подключается и подтверждает путь на
ХОСТЕ (POST /internal/projects/:projectId/workspace-verification, RN-423)
— runner — ИСТОЧНИК ИСТИНЫ пути, перезаписывающий то, что было
набрано в мастере. Engine.Actions.TerminalExecutor.decisao_de_execucao/1
получил ЧЕТЫРЕ выхода: маршрутизирует только при проверенном workspace И подключённом
runner-е; без любого из двух явно ОТКАЗЫВАЕТ — никогда не падает в
System.cmd/bind-mount от mounted, которых для проекта
runner не существует.
Находка реализации, исправляющая ADR (который не редактируется —
исправление живёт здесь и в
backlog.md):
фраза ADR 0104 о том, что конвертация между тремя режимами существующего
проекта «становится разрешённой без пересоздания проекта», НЕВЕРНА.
UpdateProjectDto по-прежнему намеренно исключает executionMode/workspacePath
— это не тривиальный PATCH (worktree, permissions.json и кеш
engine указывают на старую область). Волна 1 сдала лишь поле из трёх
значений при СОЗДАНИИ; конвертация существующего проекта — будущая волна, с
собственным замыслом, ещё не запланированная. Backlog runner-а: Волна 2 (PAT,
ADR 0105) закрыла пункт, блокировавший npm publish @brabo/runner, а
Волна 3 (ADR 0106) сдала само распространение — tsup упаковывает
apps/runner в единый dist/index.cjs (node-pty как обязательный external,
нативный binding), публикуемый на каждом финальном теге собственным
workflow (publish-runner.yml), параллельным release.yml. Настоящая находка,
проверенная эмпирически: защите автозапуска в index.ts понадобился
realpathSync на process.argv[1] — без этого main() никогда не выполнялся,
когда CLI вызывался через bin, установленный npm install -g
(symlink, который process.argv[1] никогда не разрешает через realpath, но
import.meta.url всегда разрешает). Объявленный операционный незакрытый пункт:
настоящая публикация требует, чтобы владелец продукта создал Automation Token
npm и настроил секрет NPM_TOKEN — без него workflow предупреждает и пропускает,
никогда не падает. Исключительность по {project_id, machine_id} по-прежнему отложена
до второго реального одновременного dev; best-effort guard.ts — ПОДТВЕРЖДЁННЫЙ
инвариант, а не пробел. См.
ADR 0104/
ADR 0105/
ADR 0106.
ИНСТРУМЕНТ РАЗРАБОТКИ — pnpm bootstrap
Терминальное меню в scripts/dev/bootstrap.sh, группирующее то, что делается
изо дня в день: Docker, K8s, Database и Test. Существует, потому что эти команды живут
в ТРЁХ местах, которые не разговаривают, — package.json, Makefile и скрипты в
deploy/k8s//docker/ — а заученное знание гниёт.
Оно ничего не переизобретает: каждый лист вызывает уже существующую команду.
Режим --print-commands разрешает всё дерево без TTY и без выполнения, и именно
на нём работает тест (scripts/dev/bootstrap.spec.ts) — TUI не тестируется
модульно, но отображение меню→команда — да, и ошибается именно оно.
Ноль новых зависимостей; цвета берутся из design/tokens.css в 24-битном ANSI
с деградацией до 256 цветов и до отсутствия цвета.
Вывод выполняющейся команды ПРОКРУЧИВАЕТСЯ (колесо мыши в SGR, j/k,
PageUp/PageDown, G для возврата в конец). Не хватало не чтения колеса: не хватало
СМЕЩЕНИЯ — tail -n умеет показывать только конец, — поэтому окно стало срезом
через sed -n 'a,bp', тестируемым без TTY через --print-window. Два следствия,
которые объявляет подвал: прокрутка назад ЗАМОРАЖИВАЕТ окно (как less +F, иначе
перерисовка с частотой 5 Hz отменяет прокрутку), а отслеживание мыши, включаемое только на экране
выполнения, выключается при ЛЮБОМ выходе — \e[?1006l\e[?1000l в
restaurar_terminal, который покрывает trap на EXIT. Парсер escape — общий CSI
(читает до финального байта): чтение двух фиксированных байтов оставляло остаток последовательности
в буфере, а поскольку меню трактует [1-9] как выбор, поворот колеса запускал
пункты меню.
Три зафиксированных решения: Create готовит с нуля, а Deploy публикует
в уже существующую среду (поэтому выбор по сервису есть только у Deploy);
в K8s работает только All, а Api/Engine/Web появляются ВЫКЛЮЧЕННЫМИ, а не
исчезают, потому что меню должно говорить, чего продукт не делает; и
Database › Delete пересоздаёт расширение pgvector, поскольку
docker/postgres/init.sql выполняется только при первой инициализации тома —
чистый DROP SCHEMA заставил бы следующую миграцию упасть, а поскольку engine
делит ту же базу, восстановление требует db:migrate И engine:migrate.
Docker › Reset total (scripts/dev/reset-total.sh) объединяет всю эту
последовательность в один лист — preflight, up --build --wait (
--wait из Compose v2.17+ — это «пока всё не станет здоровым», без цикла здоровья,
написанного вручную), тот же DROP SCHEMA, что у Delete, db:migrate +
engine:migrate и seed — с СОБСТВЕННЫМ экраном подтверждения
(confirmar_reset, требует набрать RESET), отличным от экрана Delete (который
требует ИМЯ базы), и без предусловия Delete «postgres уже запущен»,
потому что именно эта команда поднимает compose с нуля. Seed
(apps/api/src/db/seed.ts) получил шаг, активирующий учётные данные
provider-а из .env, переиспользуя ТЕ ЖЕ переменные
<PROVIDER>_TEST_KEY, которые уже используют smoke-тесты LLM
(apps/api/test/infrastructure/llm/*.smoke.spec.ts), — одно соглашение
об именах, два потребителя; provider без заданной переменной не входит, без
ошибки, и учётные данные остаются у OWNER-а (RN-058).
Две находки при первом запуске сброса, а не из-за самого пункта — ни одна не гипотетична, обе сломали реальное выполнение:
gemma:1bне существует в registry Ollama (manifest unknown— существует толькоgemma3:1b), поэтомуollama-model-loaderвсегда падал, и--waitникогда не закрывался зелёным. Исправлено в шести местах, нёсших неверное значение как КОД —docker-compose.yml/.prod.yml,.env.example,docker/ollama/pull-models.sh,scripts/dev/verificar-modelos-ollama.sh,deploy/k8s/base/ollama/job-model-loader.yamlи RN-415 вdocs/business-rules.md— без касания ADR 0099/0100 (принятых) и исторического повествования этого же раздела CLAUDE.md, которые описывают то, что было запрошено/решено тогда, а не текущее операционное значение.- Серьёзнее и старше —
Database › Delete(пункт 3.4, существующий с создания меню) никогда не удалял по-настоящему:DROP SCHEMA public CASCADEне достаёт доengine.*(Ecto/Oban живёт в СОБСТВЕННОЙ схеме,engine) и доdrizzle.__drizzle_migrations(управление drizzle-kit живёт в СОБСТВЕННОЙ схеме,drizzle). Результат:mix ecto.migrateупирался вduplicate_tableна выживших таблицах engine, аpnpm db:migrate— хуже, БЕЗ какой-либо ошибки — видел нетронутое управление drizzle-kit, заключал, что уже всё выполнил, и не пересоздавал НИ ОДНОЙ таблицы api.Delete/Reset totalтеперь удаляют такжеengineиdrizzle, в этом порядке, доpublic.
Сгруппированные вкладки, PR на уровне проекта, локальная папка через Runner, карусель PO (2026-08-20)
Пять независимых волн, выполненных параллельно по оспариваемому файлу, пришедших
из реального использования экрана проекта — ни одна не запланирована заранее. Шестая волна
той же программы (полный перевод продукта и документации, i18n)
больше пяти вместе взятых; её ФУНДАМЕНТ (Волна 6a — механизм, пока без
перевода содержимого) уже закрыт: react-i18next+i18next в web
(en стал языком приложения по умолчанию, pt-BR сохранён — RN-425), столбец
locale в users, встроенный в payload входа/refresh, новая AccountPage
(/account), доказывающая механизм, и Docusaurus с
i18n.defaultLocale: 'en'/locales: ['en', 'pt-BR'] — текущий снимок pt-BR
из docs/ уже сохранён в
website/i18n/pt-BR/docusaurus-plugin-content-docs/current/. Волна 6b
(массовое извлечение интерфейса + перевод docs/) сильно продвинулась
2026-08-22, на четырёх параллельных фронтах — подробности каждого см. в CHANGELOG.md (Unreleased/
Документация), — но ЕЩЁ НЕ закрылась: не хватает
docs/business-rules.md (переведён только front-matter; тело, с
сотнями RN, по-прежнему на 100% на португальском — это самый большой файл всей
документации, и он растёт быстрее, чем его можно попутно перевести) и
остаточного среза меньших компонентов .tsx. Когда она по-настоящему закроется,
этот раздел получит итоговое резюме, а остальной CLAUDE.md (Stack,
«Documentação») будет полностью пересмотрен, чтобы зафиксировать английский как
настоящий основной язык.
- Сгруппированная линейка вкладок: 11 отдельных вкладок стали 6 наверху — Visão
geral, Agentes ▾ (Executores, Criativo, Chat, Insights), Dev ▾
(Código, PRs, Aprovações), Documentação ▾ (Backlog, Arquitetura),
Gastos, Configurações — через новый
GroupedTabs(apps/web/src/components/ ui/GroupedTabs.tsx), построенный ПОВЕРХ общегоTabs, который остаётся без групп для других экранов.ChaveDeAbaостаётся плоским — сгруппировано только представление, чтобы не трогать deep-link?tab=. Chat и Chat RAG стали ОДНОЙ вкладкой (ProjectChatShell.tsx) с внутренним сегментированным управлением («Conversar»/«Buscar») — бизнес-различие между ними (RN-202: разговор активирует агента и тратит ключ владельца за ход; RAG — чтение по индексу, без агента) не изменилось, изменился только UI-контейнер. - Вкладка PR, на уровне проекта:
ProjectApprovalsTabограничивал «PRs em revisão» самой свежей сессией — ревью PR из предыдущей сессии исчезало с экрана, как только новая сессия предлагала другой. Новая вкладкаprsперечисляет напрямую из git-provider-а (никогда по сессии) и сверяет с ожидающимproposed_actionчерез новыйfindPendingByProject, рядом с уже существующим методом с областью сессии — решение используетsessionIdСАМОГО действия, никогдаlatestSession(RN-423). Это первый настоящий производительgit_mergeчерез UI; блокировка защищённой ветки (RN-154) остаётся абсолютной и нетронутой — кнопка «Merge» лишь ПРЕДЛАГАЕТ, человеческое одобрение по-прежнему единственный способ выполнить. - Вкладка Arquitetura: выделена из Обзора (module_map, диаграмма C4,
ADR, незакрытые пункты), в котором остаётся сжатое резюме + ссылка. Первый
lightbox дизайн-системы —
Modalполучилsize="full"— для увеличения диаграммы C4 (SVG, без потери качества) (RN-424). - Навигация по локальной папке через Runner (RN-422, ADR 0104): пересматривает
ADR 0072 (который явно отклонил выбор папки), НЕ
редактируя его — навигация существует, но через Runner, который уже работает с
реальными привилегиями пользователя на его машине, никогда через api, перечисляющую
файловую систему контейнера. Два новых события в уже существующем канале
(
fs_list_dir/fs_home_dir), чистый relay engine, тот же шаблон, что у PTY.FolderBrowserModalработает там, где проект уже существует. При создании (NewProjectWizard.tsx) пробел был объявлен до ADR 0108: в режимеmountedпроект по-прежнему рождается только при подтверждении (проверка пути там касается диска), но в режимеrunner«Procurar pasta...» начинает создавать проект ЗАРАНЕЕ — переиспользуемый по снимку идентичности, а не дублируемый при каждом клике, — чтобы можно было привязать тикет канала до подтверждения (RN-437, ADR 0108). НовыйRunnerOnboardingPanel(общий с вкладкой Terminal) заменяет прежний сырой<code>.apps/runnerполучил README — настоящая публикация в npm/упаковка подписанного бинарника остаётся вне объёма, объявлено в README. Сам проводник папок стал трёхколоночным (ярлыки, список, где один клик выбирает/двойной клик входит, панель деталей — RN-436), следуя визуальному эталону владельца продукта. - Баг карусели PO исправлен (RN-421): партия ожидающих продвижений
зависела от сканирования последних 200 событий
(
useSessionEvents) — в длинной сессии предложение выпадало из окна, и карусель деградировала до одной карточки или исчезала. Тот же класс бага, который RN-180 уже исправила дляContextAside; источник теперь —useBacklog(полный, без окна), с откатом по истории, когда backlog ещё не ответил.
Ручной handoff выбранному агенту (RN-440/441, ADR 0109)
Закрывает пункт backlog, открытый с ФАЗЫ 13c. CreateHandoffUseCase
остаётся ЕДИНСТВЕННЫМ местом, которое записывает toAgent (ADR 0038), — он получил лишь
опциональный actor?: Actor, по умолчанию привычный {kind:'agent', id: fromAgent}.
RequestManualHandoffUseCase (новый маршрут, POST .../sessions/:sessionId/handoffs, роль developer) проверяет toAgent
против addressableAgents() — ЗАКРЫТОГО каталога (lead-ы областей ∪
SOLO_CONVERSATIONAL_AGENTS), строже, чем assertHandoffTargetAllowed
(который отклоняет только субагента), — разрешает fromAgent по самому свежему
agent.activated сессии (sentinel "usuario", если ни одного), и записывает
actor: {kind:'user'}. Handoff рождается offered, как и автоматический:
уже существующая карточка принятия (SessionPage.tsx) подхватывает его сама, ноль
изменений на пути принятия.
Расследование ДО написания кода нашло ux-designer (ADR 0087) в ТОЙ ЖЕ
ситуации, что и Staff (ADR 0088), которая мотивировала пункт: клауза message/2
готова в agent_command_controller.ex, никакого человеческого пути к ней.
Оба вошли в AGENTES_DE_CHAT в одном изменении — infra/qa
остаются вне, по тому же уже задокументированному там шаблону (lead-ы без kickoff/1
и без клаузы message). Объявлено, не скрыто: зеркало
SOLO_CONVERSATIONAL_AGENTS на стороне web (apps/web/src/lib/agents.ts)
НЕ сверяется тестом с зеркалом api — расхождение даёт в худшем случае
устаревший вариант в селекторе, который backend всё равно отклоняет с 400.
Бюджет по области — закрывает отсечение ADR 0038 (RN-443, ADR 0110)
Одобренный пункт backlog («Older backlog», docs/explanation/backlog.md).
agent_areas получает budget_micros/spent_micros, в точности отражая
max_parallel — та же строка, тот же владелец решения (maintainer), без
новой таблицы. Это ТРЕТИЙ ДОБАВОЧНЫЙ предел рядом с проектом и сессией
(CheckBudgetGateUseCase проверяет все три параллельно, любой заблокированный
уже отказывает), а не каскад binding модели из ADR 0064 (sessão > agente > área > projeto > workspace, «побеждает наиболее конкретный») — два
механизма разделяют слово «область» и больше ничего; не путать их —
центральная мысль ADR 0110, так же как RN-101/ADR 0063 уже
отделяет представление владельца от представления участника в отчёте о расходах.
spent_micros суммирует ВСЕГДА, когда актор принадлежит области (lead или
участник, через areaDo — чистая функция, без обращения к базе для актора без области),
с настроенным budget_micros или без (null — значение по умолчанию, без предела).
Автономный бинарник runner-а (2026-08-23, RN-451/452, ADR 0112)
Пункт backlog ADR 0104 («standalone binary, pkg/bun build --compile»), спутник ADR 0106 (распространение через npm). Явное решение
владельца продукта: по-настоящему ЕДИНЫЙ бинарник, полная матрица
с Windows с v1 — более дешёвая альтернатива («бинарник + папка
node-pty-native/ рядом») была предложена и отклонена.
bun build --compile встраивает нативный .node из node-pty через
with { type: 'file' } и извлекает его в НАСТОЯЩИЙ ВРЕМЕННЫЙ каталог в
runtime (native-pty-loader.ts) — необходимо, потому что node-pty разрешает
собственный addon через require() по ВЫЧИСЛЯЕМОМУ пути
(lib/utils.js#loadNativeModule), который Bun не умеет встроить сам
(build проходит, runtime выбрасывает Cannot find module, протестировано до
проектирования исправления). Пять платформ
(linux-x64/linux-arm64/darwin-x64/darwin-arm64/win32-x64), каждая
собрана на своём НАТИВНОМ runner-е
(.github/workflows/build-runner-binaries.yml, триггер финального тега,
тот же шаблон, что publish-runner.yml), бинарник прикреплён к уже
существующему Release.
Честная проверка, по платформам: только linux-x64 был действительно
выполнен в этой среде (build:bin+smoke:bin зелёные, многократно —
именно при запуске настоящего бинарника проявились два бага ниже). Остальные
четыре — особенно Windows, с ТРЕМЯ нативными файлами вместо одного —
были продуманы чтением исходного кода node-pty, ни разу не выполненные
на реальной машине той ОС; их первое настоящее выполнение — СЛЕДУЮЩИЙ
тег версии, а не этот PR. Ни один платформенный fallback не был применён
превентивно — ничто в этом расследовании не ДОКАЗАЛО, что какая-либо из пяти не
работает так; если настоящий тег покажет, что да, это станет новым пунктом, а не
сегодняшним предположением.
Два дефекта, найденные только запуском скомпилированного бинарника, а не чтением API:
--external node-pty должен быть в команде bun build, даже при отсутствии
какого-либо статического import node-pty в коде — fallback await import(...)
(используемый только вне бинарника) всё равно обнаруживается и встраивается Bun
и падает в runtime с обманчивой ошибкой; и защите
автозапуска (ADR 0106) понадобилась вторая ветка — process.argv[1] внутри
бинарника — ВИРТУАЛЬНЫЙ путь (/$bunfs/root/...), до которого realpathSync
не дотягивается, выбрасывая ENOENT вне любого try/catch до того, как
main() будет вызван.
Конвертация execution_mode существующего проекта (RN-447..450, ADR 0111)
Закрывает исправление, которое Волна 1 runner-а (ADR 0104) зафиксировала в
docs/explanation/backlog.md: пункт 4 того ADR говорил, что конвертация
execution_mode между container/mounted/runner в уже существующем проекте
«становится разрешённой без пересоздания проекта», и до сих пор это было ложью —
UpdateProjectDto по-прежнему намеренно исключает оба поля.
PUT projects/:projectId/execution-mode (maintainer,
ConvertProjectExecutionModeUseCase) — ВЫДЕЛЕННЫЙ маршрут, а не ослабленный
PATCH: он оркестрирует миграцию, а не просто меняет столбец.
Отказывает с 409, пока любой dev agent проекта (в ЛЮБОЙ сессии,
прочитанный напрямую из engine.dev_agent_states по project_id, тот же путь, что у
RN-409) не находится в idle — idle_tripped здесь СЧИТАЕТСЯ активным, в
отличие от критерия «онлайн» RN-409, потому что у агента ещё есть
workspace_root, захваченный в памяти в ожидании человеческой разблокировки
(RN-447). PermissionsFileStore получил move(from, to): содержимое
permissions.json никогда не меняется, меняется только физический путь, потому что projectScopeRoot
выводится из пары (режим, путь) (RN-448). При выходе из container с
подготовленным контейнером жизненный цикл (ADR 0081) доводится до removed — через
stopped сначала, если был running, — ДО изменения столбца, потому что
RegistrarTransicaoDeContainerUseCase отклоняет переход вне режима
container; вход В container никогда не готовит автоматически (RN-449).
workspaceVerifiedAt обнуляется при каждой настоящей конвертации, в том числе при возврате в
runner с внешне одинаковым путём (RN-450). Та же (режим, путь), что и
сегодня, — no-op, даже не проверяет активного dev agent.
Объявленное, не скрытое следствие: конвертация НИКОГДА не копирует
уже написанный код между старым и новым корнем. Это безопасно для того, что уже
ЗАКОММИЧЕНО — engine выводит worktree из bare-репозитория с нуля
(Engine.Actions.Workspace.ensure!/4) всякий раз, когда папка назначения ещё
не помечена готовой, поэтому следующий агент перестраивает рабочее
дерево в новом корне из настоящего git. Что НЕ выживает — это
НЕзакоммиченный diff в брошенном worktree: он остаётся сиротой на старом диске,
никогда не мигрируемый и не удаляемый. Предел активного dev agent (RN-447) снижает
риск (никогда не конвертирует при агенте working), но не устраняет: агент
idle между tasks может сидеть на грязном diff, и ничто здесь
не проверяет чистоту рабочего дерева перед конвертацией.
Локальная папка, прикреплённая как справочник Chat RAG (2026-08-23, RN-455..457, ADR 0113)
Запрос владельца продукта: прикрепить папку СОБСТВЕННОЙ машины пользователя к
проекту как справочник ДЛЯ ЧТЕНИЯ агентами — без CLI
brabo-runner (ADR 0103). Явно НЕ является выполнением/workspace
проекта: execution_mode: runner (ADR 0104) нужен CLI, потому что нужен
настоящий путь ХОСТА, чтобы контейнер маршрутизировал команду; это больше
похоже на «прикрепить файлы к разговору». Браузер читает СОДЕРЖИМОЕ
(File.text()) и ОТНОСИТЕЛЬНЫЙ путь (File.webkitRelativePath)
выбранных файлов — никогда абсолютный путь хоста (API File
не открывает его ни одному сайту), и поэтому ADR 0113 прямо говорит, что это НЕ
пересмотр того, что отклонили ADR 0072/0107: те отклоняли
РАЗРЕШЕНИЕ пути хоста из браузера; здесь не существует никакого пути
для разрешения, только байты, которые браузер уже имел право читать.
chunks.scope (ADR 0079/0080) получает ЧЕТВЁРТОЕ значение, 'local' (миграция
0052, ALTER TYPE ... ADD VALUE, добавочное — оба CHECK миграции
0045 уже записаны как «это session или нет», поэтому local попадает на
ту же сторону, что docs/adr, без нового CHECK). IndexLocalFolderUseCase
близко отражает IndexProjectDocsUseCase — Markdown-aware чанкинг для
.md/.mdx, RagEmbeddingService, чанк без вектора вместо падения,
когда embedding отваливается (RN-233), — но ОТКЛОНЯЕТ (400) всю загрузку,
когда АГРЕГИРОВАННЫЕ пределы превышены (количество файлов, суммарные
байты), никогда не обрезает молча: это ЕДИНИЧНЫЙ жест пользователя с
селектором папки на экране, а не фоновое сканирование. Отдельный слишком большой файл
или файл с нераспознанным расширением лишь ПРОПУСКАЕТСЯ
(filesSkipped), никогда не роняет партию (RN-456). origemDoChunk не
понадобилась никакая новая ветка: local несёт sourcePath, как
docs/adr, и попадает в kind: 'file', который цитирование уже умело отображать.
Никогда не входит в общее «Reindexar agora» (RN-457): ReindexProjectUseCase
переиндексирует, читая из источника, который СЕРВЕР может снова посетить (репозиторий
проекта, event log); у local нет никакого источника для повторного посещения —
текст существует только в браузере того, кто прикрепил. Повторное прикрепление папки (новая
загрузка в POST .../rag/local, maintainer) И ЕСТЬ механизм
повторной синхронизации, намеренно отдельная кнопка, с перекрёстным комментарием
в обоих вариантах использования, указывающим друг на друга и на ADR 0113, чтобы будущее
«исправление» не связало их снова и не стёрло материал пользователя.
Покрытие (RagCoverage.local) — СОБСТВЕННОЙ формы — нет «итога в
репозитории» для сравнения — и lastAttachedAt — ЕДИНСТВЕННОЕ настоящее исключение из
правила «никогда X мин» (RN-237): настоящий MAX(chunks.created_at), а не
угаданный (RN-458).
Полоса активности хода — нить получает ответ только в конце (RN-459/460)
Запрос владельца продукта: экран Сессии теперь показывает НАД
composer полосу, которая человеческим языком рассказывает, что разговорный агент
(Criativo, PO, Arquiteto, Dev Lead, UX Designer, Staff)
делает ВО ВРЕМЯ хода — визуальный эталон: строка статуса
Claude Code. Нить получает пузырь ответа только ПОСЛЕ того, как ход
заканчивается; правило относится к ЧАТУ (канал Phoenix) и действует только для шести
разговорных агентов — консультативный чат без активного агента (SSE,
streamChatMessage) остаётся с привычным потоковым пузырём,
нетронутым.
Механизм: шесть серверов уже выдавали ДОЛГОВЕЧНЫЙ tool.call; теперь они
также делают broadcast(state, "tool.call", %{tool: name, agent: @agent}) —
ЭФЕМЕРНЫЙ, без args (RN-096/RN-412), — чтобы канал доставлял в реальном времени.
Нет общего behaviour/макроса между шестью dispatch_tool
(четыре разные структурные формы, подтверждено полным чтением) —
изменение — шесть правок, приспособленных к локальной форме каждого, а не
рефакторинг. На стороне web ЧИСТЫЙ reducer
(reduzirAtividadeDoTurno/lib/atividade-do-turno.ts) накапливает
agent.delta как текущий текст; при приходе tool.call архивирует
непустой текущий как строку повествования, обнуляет и всегда добавляет
строку инструмента с fraseDaFerramenta(tool) (словарь 19
инструментов, lib/narracao-de-ferramentas.ts, без собственной RN). СОБСТВЕННЫЙ
флаг (turnoViaCanal, никогда не выводимый из statusAgent/streamingAgent,
которые законно проходят через окна null посреди хода) решает, появляется ли
полоса или старый пузырь — никогда оба.
Правило представления (история): agruparNarracoesDoTurno, новый
чистый проход после afundarDesfechos (RN-172) в том же useMemo —
последовательные agent.response одного turno+autor становятся компактным
Disclosure («Passos do turno · N»), только последний остаётся нетронутым
вне его. turno === 0 (сторожевой пролог turnoDoSeq) остаётся
ВНЕ группировки — старые fixtures складывают agent.response без
границы хода, чтобы тестировать другой механизм (RN-138/RN-177), и без
исключения новая группировка дала бы там Disclosure внутри
Disclosure. Сам afundarDesfechos не изменился.
Находка по пути, объём расширен по решению владельца продукта:
расследование выяснило, что ЧЕТЫРЕ из шести серверов (Arquiteto, Dev Lead, UX
Designer, Staff) заканчивали МОЛЧА на пределе итераций —
defp run_turn(state, remaining) when remaining <= 0, do: state, без
какого-либо события, вопреки постоянному правилу, что цикл агента не
заканчивается молча. Только PO уже выдавал toolloop.limit_reached (RN-166);
Criativo намеренно расходится с agent.error (решение задокументировано
в самом коде). Четыре исправленных теперь выдают
toolloop.limit_reached, то же событие, что и PO (RN-459).
Перенесено из индекса CLAUDE.md 2026-09-11
Записи ниже пришли из таблицы-индекса CLAUDE.md и не имели
собственного раздела в этом файле — их повествование существовало только в одном месте. Они были
перенесены без переписывания, в том порядке, в каком были, когда таблица была сведена к
индексу. То, что здесь, — история; то, что действует сегодня, живёт в RN, в ADR
и в разделе «Estado atual e aberto» файла CLAUDE.md.
ПРОГРАММА 16–26 (ADR 0060–0072)
Принят дизайн-handoff; 8 экранов; kind сессии; пагинированные события; Gastos; модель по области; контейнер на проект (25b отсечена); вкладка Code
pnpm bootstrap
Терминальное меню поверх существующих команд; Reset total
Нативный Ollama / pull из Hugging Face (RN-461..463, ADR 0114–0115)
Bootstrap dev спрашивает один раз и сохраняет режим Ollama в .env;
навигатор моделей Hub с pull в два этапа и allowlist официальных
издателей
Собственный lockfile сайта (ADR 0117)
website/ выходит из корневого workspace pnpm — lockfile и overrides безопасности
изолированы, pnpm audit продукта перестаёт сообщать о дереве Docusaurus
Автоматическая настройка runner-а через браузер (RN-464..466, ADR 0118)
Ключ устройства Ed25519, генерируемый в браузере, в дополнение к PAT; прокси
бинарника через GitHub Releases; File System Access API записывает настроенную папку,
с откатом на загрузку вне Chromium; --project/--dir/--token становятся
опциональными в CLI
Образы, опубликованные в GHCR (ADR 0119)
release.yml публикует четыре образа на каждом финальном теге и записывает digest-ы
в .release/images.json; overlay production перестаёт указывать на
заглушку
Схема по агрегату (ADR 0121)
db/schema.ts становится barrel; 51 таблица и 34 enum становятся 16 файлами под
db/schema/, по одному на агрегат domain/*
SessionPage.tsx в 5 механических PR (ADR 0122)
3 807 → 2 661 строка; временная шкала/ход, StorySlide, StructuredQuestionCard,
дерево backlog + ContextAside, hook useSessionReadiness — кластер
канала хода и ProjectSettingsTab.tsx остаются вне, объявлено
Hook канала хода SessionPage.tsx (ADR 0124)
2 661 → 2 479 строк; механический PR дедупликации
(iniciarTurnoDoAgente/finalizarTurnoDoAgente/cancelarTurnoOtimista),
за которым следует вынос состояния + эффекта канала Phoenix в hook
useTurnoDoAgente (lib/session-turno.ts) — закрывает пункт, который ADR 0122
оставил объявленным открытым; ProjectSettingsTab.tsx остаётся вне, отдельное
решение
ProjectSettingsTab.tsx по разделам (ADR 0125)
2 532 → 77 строк; 17 разделов становятся по файлу каждый под routes/settings/
(+ shared.ts с ДВУМЯ helper-ами, у которых больше одного вызывающего), а старый файл
остаётся на пути как вход и barrel — путь и 11 экспортируемых имён являются
контрактом 3 тестов. Закрывает ПОСЛЕДНЮЮ половину строки долга в
docs/architecture.md
Вертикальная дорожка навигации проекта (ADR 0126, RN-201)
Горизонтальная двухуровневая линейка (GroupedTabs) становится вертикальной дорожкой со
ВСЕМИ ТРЕМЯ группами, открытыми одновременно; геометрия дорожки CodeShell,
перенесённая клавиатура, 5 счётчиков остаются раздельными. Пересматривает RN-201: вкладка
Código больше не сворачивает боковую панель сама
Якорное оглавление Настроек (CHANGELOG)
17 разделов вкладки получают id, <section> с доступным именем и оглавление
в четырёх группах ВНУТРИ области содержимого — без новой вертикальной полосы, рамка
остаётся в 444px из ADR 0126. Реестр в routes/settings/sumario.ts
(третий список того же замысла, что project-tabs.ts), deep-link ?section=,
spy через IntersectionObserver; в оглавление входит только СМОНТИРОВАННЫЙ раздел
Единый шаблон унаследованного значения (CHANGELOG)
ЧЕТЫРЕ формулировки «нет собственного значения» во вкладке Настроек становятся двумя
полюсами и одним глаголом, из одного источника (routes/settings/heranca.tsx): метка говорит
о СОСТОЯНИИ, деталь — о ПОСЛЕДСТВИИ. Словарь унифицирован, форма нет —
метка входит только туда, где элемент управления не показывает состояние сам
Сохранение по разделу в Настройках (RN-469)
Параллелизм и Предел расходов заменяют N кнопок по строкам ОДНОЙ кнопкой раздела
(settings/secao-salvavel.tsx), со счётом ожидающих строк и исходом
ПО СТРОКЕ — сохранение — это N вызовов, а не транзакция, и экран никогда не утверждает
то, чего не получил. Три раздела с автосохранением и Учётные данные остаются вне, каждый
по объявленной причине
Панель «нужно ваше участие» (RN-467)
Chip наверху проекта открывает ПЯТЬ очередей решений в одном месте —
раздельно, упорядочено по срочности, без какого-либо суммирования (даже в chip). Одобрения
и merge решаются там через ApprovalCard; остальные три ведут во вкладку.
Незакрытый пункт архитектуры заимствует дату истории и ГОВОРИТ, что заимствовал
Каскад модели как видимая цепочка (RN-470)
Столбец Origem перестаёт печатать enum базы и становится workspace › projeto › área › agente с четырьмя состояниями на узел (settings/cascata.tsx) — разделяя
ДВА смысла origin: 'agent', не трогая api. Предупреждение об отброшенном
уровне входит в цепочку; три — получают три текста. Закрывает канвас
улучшений UI (взяты 7 из 7 пунктов)
Папка до runner-а (RN-473/474)
Настройка runner-а через браузер (ADR 0118) меняет порядок:
showDirectoryPicker — ПЕРВЫЙ шаг, а бинарник — ПОСЛЕДНИЙ, best-effort —
релиз без asset возвращал 502 до открытия селектора, и папка становилась
недостижимой. Сбой бинарника сохраняет оба записанных файла и меняет
инструкцию на путь npm install -g @brabo/runner. После инструкции
EsperaDoRunner опрашивает workspaceVerifiedAt с тремя состояниями и пределом
kid в ключе устройства (RN-475)
Автоматический режим ADR 0118 НИКОГДА не аутентифицировал: браузер отбрасывал id,
который возвращала регистрация, и записанный JWK рождался без kid — CLI его
всегда отклонял. Регистрация и экспорт становятся ОДНОЙ функцией
(registrarChaveEExportarPrivada), на ОБОИХ путях; и отказ CLI перестаёт
быть неотличимым от «ключа нет». Тест, который это пропустил, утверждал,
что файл был ОТКРЫТ, а не его содержимое
Provisioning, который не молчит (RN-477)
Два сбоя не превращались ни в какое состояние — step.check вне try и отказ
createRepo ДО появления строки bootstrap, — а экран опрашивал
«Iniciando…» вечно без кнопки. Корневая причина всего: /data/git-repos и
/data/project-workspaces рождались root в образах DEV, которые
Dockerfile.prod уже умел создавать до USER
Файл политики и область терминала (RN-478)
У projectScopeRoot было ДВА потребителя с противоположными потребностями, и режим
runner (без bind-mount) их разделил: область по-прежнему указывает на ХОСТ,
permissions.json начинает жить в УПРАВЛЯЕМОМ корне. Активация выполнения
возвращала 500 (mkdir '/home/<usuario>'), а ЧТЕНИЕ молча деградировало — в
проекте runner файла никогда не было. Заодно: типизированная ошибка с 400 вместо
сырого Error, Обзор перестаёт проглатывать сообщение api, и аннотация
OpenAPI, обещавшая 409 для двух случаев, которые никогда не были 409
Предупреждение о человеческом шаге до клика (RN-473/477)
Шаг терминала runner-а объявлялся только в состоянии успеха; теперь он
называется и в начальном, а финальная команда получает cd <caminho>, когда можно
утверждать, что выбранная папка — папка проекта. Метка кнопки НЕ меняется —
она говорит о папке, которая действительно автоматическая
Одна модель для всех агентов (RN-476)
Таблица Modelos por agente получает панель, применяющую ОДНУ модель ко всем 17
разом, записывая на уровне АГЕНТА (на уровне проекта это был бы maintainer, и
затронуло бы значение по умолчанию сессии). Первое ИСКЛЮЧЕНИЕ из линейки «названное значение сохраняется
в onChange» из RN-469 — селектор является аргументом действия, а не конфигурацией, —
с исходом в трёх состояниях из самой RN-469
Пределы понижения в project_members (ADR 0127, RN-472)
Перекрытие projectRole ?? workspaceRole ОСТАЁТСЯ в обоих направлениях (это
способность, а не баг); входят два предела 403 в варианте использования —
никто не понижает owner workspace, никто не понижает самого себя. Три
описания OpenAPI, которые RN-471 объявила ложными, начинают описывать
код; gate для Select — отдельный PR, поскольку экран не может вычислить
первый предел
Телеметрия поиска RAG (RN-479..481)
Гибридный поиск оставляет след: rag_searches (с ЗАМОРОЖЕННЫМИ весами в
строке) и rag_feedback (голос полезно/нерелевантно, единственный настоящий сигнал).
ТАБЛИЦА, а не только событие, потому что session_events.session_id — NOT NULL, а
у поиска из вкладки нет сессии — событие rag.search/rag.feedback — ПОВЕСТВОВАНИЕ,
только когда есть сессия. Инструмент rag_feedback (:direct) у шести агентов,
у которых уже был rag_search, и medir:rag для чтения. НИЧЕГО не откалибровано — этот
этап лишь инструментирует
Broker контейнеров (ADR 0130, RN-485/486)
Рождается apps/broker, ЕДИНСТВЕННЫЙ процесс, говорящий с демоном Docker на
сервере, — и он НЕ принимает спецификацию: получает projectId + операцию, ЧИТАЕТ
решение Arquiteto из api и СОСТАВЛЯЕТ образ, сеть, ресурсы и единственный mount.
Порт ADR 0128 ПЕРЕЕЗЖАЕТ из apps/runner/src/ в packages/docker-port (оба
потребителя его упаковывают; api не может его потреблять). Маршрут жизненного
цикла начинает возвращать наблюдаемое рядом с зарегистрированным, не сливая
Маршрутизация модулей в infra (PR 1.4) (ADR 0131, RN-487)
Седьмой инструмент Arquiteto, route_modules_to_infra: элемент {modulo, imagemCandidata, porque} на модуль действующего module_map, записанный как
artifact.module_routing (без таблицы, тот же замысел, что у
artifact.project_image/artifact.c4_diagram). Он ВЫДВИГАЕТ КАНДИДАТОВ — образ
каждого элемента проходит через ту же validarDecisaoDeImagem, что у choose_project_image,
— а ВЫБИРАЕТ среди кандидатов Infra, в отдельном PR (1.5, закрыт
чуть ниже)
Golden-set точности RAG (ADR 0132, RN-490)
Шаблон golden-set QA (ADR 0123), применённый к гибридному поиску: поскольку
суждение живёт не в engine (это HybridSearchUseCase, в api),
seed-golden-set-rag.ts готовит ОДИН проект с реальным курированным корпусом (22
файла из docs/) И запускает поиск для 17 вопросов, составленных из реальных RN/ADR;
rag_golden_test.exs лишь вызывает скрипт и применяет пол. Критерий
попадания — путь файла, top-5, никогда точный chunk/rank 1 — чистая функция,
протестированная. Новый gate rag-acertivo, warn (без CI с LLM). Измерено по-настоящему,
дважды, детерминированно: 17/17 в top-5
Dev agents выполняются ВНУТРИ настоящего контейнера (PR 1.6) (ADR 0134, RN-492/493)
Закрывает то, что RN-491 объявила незакрытым: Engine.Actions.TerminalExecutor
получает ПЯТЫЙ выход, :executar_no_container — проект execution_mode: container с ЗАРЕГИСТРИРОВАННОЙ строкой running
(Engine.Containers.ProjectContainerLifecycle, прямое чтение, тот же шаблон,
что у Engine.Projects.Project) — и команда проходит engine → api (POST internal/projects/:projectId/container-exec,
ExecutarComandoNoContainerUseCase) → broker (ContainerBrokerPort.exec,
ПЕРВЫЙ настоящий вызывающий последней из пяти операций ADR 0128/0130),
выполняясь через docker exec вместо локального System.cmd. cwd переводится из
пути ХОСТА внутрь /work в engine, ДО вызова — подтверждено
в коде, что это ТОТ ЖЕ физический каталог, в который Workspace.ensure!/4 уже пишет
(worktree dev agent не меняет ни места, ни механизма).
BrokerRecusouError/BrokerIndisponivelError/сбой транспорта становятся
обычным failed_result, никогда не падением и не молчаливым откатом за пределы
контейнера (RN-486: зарегистрированный running никогда не гарантирует запущенный контейнер).
Терминал ВНУТРИ контейнера получает ПОЛ автоодобрения
(ProposeActionUseCase/decide.ts, containerExecutionActive) — пять
абсолютных пределов и привычная лексическая область продолжают работать поверх, байт в
байт
Runner поднимает контейнер проекта (PR 1.3, Часть 1 вне порядка — закрывает половину, которую RN-494/PR 1.7 оставил объявленной) (ADR 0137, RN-497)
container_start/container_stop/container_remove получают ВТОРОЙ путь
выполнения, ветвящийся по executionMode: container идёт через broker
(без изменений); mounted/runner начинают просить ПОДКЛЮЧЁННЫЙ runner, через ТРИ
новые пары в канале Phoenix (container_start/_result,
container_stop/_result, container_remove/_result, тот же шаблон, что
exec/exec_result) — EngineWeb.ContainerCommandController передаёт в
RunnerRouter, которому runner отвечает через DockerViaCli (ЕГО Docker,
@brabo/docker-port, получающий реальное использование помимо --self-test-docker).
Образ ЧИТАЕТСЯ (ObterSpecDeContainerUseCase, тот же вариант использования, что у broker-а),
никогда не выбирается заново. «Нет runner-а»/«timeout» (RunnerNaoConectadoError) и «runner
отказал» (RunnerRecusouContainerError) становятся именованным failed, никогда
исключением. Engine.Actions.TerminalExecutor не получил нового выхода — он уже
безусловно маршрутизировал каждую команду подключённого проекта runner в канал;
выбор хост-или-контейнер — ВНУТРЕННИЙ для runner-а
(EstadoDoRunner.containerAtivo, tratarExec маршрутизирует через docker exec с
cwd, переведённым заменой префикса, cwdParaContainer). guard.ts/RN-434
начинает покрывать bind-mount без КАКОЙ-ЛИБО новой проверки — mount И ЕСТЬ корень, уже
подтверждённый при старте CLI
Handoff Infra нельзя было принять ни на одном экране (D0) (RN-499)
offeredHandoff фильтрует карточку нити по AGENTES_DE_CHAT, который исключает
infra — и фильтр ВЕРЕН (у engine нет клаузы message для
Infra Lead; его расширение заставило бы экран предлагать несуществующую нить, а composer
— слать сообщение Criativo). Дефектом было следствие, которое комментарий
самого кода записал как принятое: у acceptHandoff был ОДИН потребитель,
за этим фильтром, поэтому handoff от OfferInfraHandoffUseCase оставался
offered навсегда, Infra Lead никогда не активировался,
propose_container_start никогда не вызывался, и НИ ОДИН проект ни в каком режиме
не доходил до запущенного контейнера. Исправление: СОБСТВЕННАЯ исполнимая карточка, вне нити,
в фиксированной полосе между прокручиваемой областью и composer — той же, что уже содержит
ручной handoff и объявляет себя местом действий handoff, не являющихся разговором,
— вызывающая уже существовавший acceptHandoff, закрытая тем же activeFor.
handoff.offered Infra по-прежнему РАССКАЗЫВАЕТСЯ в нити как немой разделитель
Единая база смонтированных проектов (PR 1, BREAKING) (ADR 0141, RN-500)
Создание проекта mounted требовало строки bind-mount, написанной ВРУЧНУЮ в api
И engine, плюс перезапуска обоих — что убивает каждый ход агента, сокет
терминала и вызов LLM в полёте во всей установке, ради подключения ОДНОГО проекта.
Рождается BRABO_PROJECTS_BASE: ОДНА база, настраиваемая оператором один раз,
монтируемая по ИДЕНТИЧНОСТИ ($X:$X) в обоих сервисах, со всеми смонтированными
проектами внутри неё и НИЧЕГО не редактируется по проекту. Идентичность, а не фиксированная
точка монтирования, потому что workspace_path набирается пользователем и показывается ему обратно
— и это то, что позволяет projectScopeRoot/workspace_dir/2 оставаться верными без
нового кода. СОБСТВЕННАЯ переменная (коллизия пространства имён с UNIQUE workspace_dir_name
привела бы к git init в папке другого проекта; противоположный владелец; база
навигируема, а управляемый корень не должен быть таковым). ОТСУТСТВИЕ — норма: projectsBase: null (GET workspaces/:id/projects-base, maintainer), и режим не
предлагается. И pnpm dev ОТКАЗЫВАЕТСЯ стартовать при базе, пересекающейся с checkout-ом, —
единственная возможная проверка, потому что api сравнивает с /workspace. Две объявленные
цены: symlink под базой и только одна база
Контейнер смонтированного проекта (PR 3) (ADR 0144, RN-503)
Проект mounted не мог получить НИКАКОГО контейнера из-за двух независимых
блокировок — api отправляла каждый не-container режим в RUNNER (который требует
подключённого brabo-runner), а broker отклонял не-container в источнике. Обе
касались ГЕОМЕТРИИ, а не имени режима, и единая база ADR
0141 изменила геометрию. Ветвление становится по НАЗНАЧЕНИЮ: container И
mounted → broker, только runner → runner (и container_stop/_remove меняются
ВМЕСТЕ с _start, иначе поднимется на сервере и остановится на машине пользователя).
Инвариант ADR 0130 не сдвигается — ни один абсолютный путь не пересекает сеть:
broker получает ВТОРОЙ корень (BRABO_PROJECTS_HOST_BASE), а api отправляет
РАЗЛИЧЁННЫЙ локатор (gerenciada |montada |indisponivel), говорящий,
относительно какого корня действует относительный сегмент. ТРИ варианта, потому что
у indisponivel два разных исправления (runner × устаревший mounted вне
базы), и недостающий корень НИКОГДА не подменяется другим. mounted ВЫБИРАЕТ
образ, как container, — broker составляет из artifact.project_image, поэтому
незаписанный выбор — инертный выбор (ADR 0133 во втором режиме)
Dev-агенты только после контейнера (PR 7) (ADR 0143, RN-502)
В реальном выполнении exp001 ДЕСЯТЬ dev-tasks застряли разом — и
находкой была не упавшая папка, а то, что ничто не упорядочивало контейнер до
dev agent. Engine.Dev.AgentIo.try_claim/2 — ЕДИНСТВЕННАЯ точка claim — начинает
запрашивать ProjectContainerLifecycle.running?/1 (предикат, который ADR 0134
уже имел) ДО вызова api: без ЗАРЕГИСТРИРОВАННОЙ строки running агент падает
в :idle, сохраняется и выдаёт dev.blocked_by_container. В ENGINE, а не только в
activate-execution api, потому что РЕГИДРАТАЦИЯ доходит до try_claim/2, не
проходя ни через какой маршрут (init/1 → finish_restart_recovery/1). :idle, а
НЕ новый статус, по причине, которую сам docblock try_claim/2 уже
записывал с Фазы 12b, — это единственное состояние, из которого wake ещё спасает,
и все guard-ы handle_info/2 на это опираются; «пустую
очередь» от «нет контейнера» отличает СОБЫТИЕ, никогда статус. Wake приходит через outbox:
RegistrarTransicaoDeContainerUseCase публикует container.running в ТОЙ ЖЕ
транзакции, которая записывает приход в running, в НОВОМ aggregate_type
(container, третий, который читает Engine.Outbox.Drain), вместо того чтобы одалживать task,
с ПРОЕКТОМ как aggregateId; доставляемое сообщение — уже существовавшее {:wake, :became_claimable}. Вторая половина: TerminalExecutor перестаёт
молча деградировать — container без running падал в System.cmd ВНУТРИ
процесса engine, а теперь ОТКАЗЫВАЕТ, с mounted в той же ветке
Тревога merge конвейера с получателем (ADR 0139)
Правило «продвижение — это --no-ff» было нарушено ТРИЖДЫ (#367, #394, #464 —
третий был PR, исправлявшим первые два). Находка: обнаружение УЖЕ
существовало и УЖЕ сработало (tag-release провалил первые два с
правильным сообщением) — не хватало КОМУ звонить, потому что workflow push, падающий
на постоянной ветке, не имеет PR, где стать красным, а в репозитории было ноль issues.
tag-release получает job avisar, который открывает issue (и комментирует в уже открытом,
никогда не дублирует) с github.token и никогда с PAT — недействительный PAT — один из
сбоев, о которых он сообщает, — и это перестало быть гипотетическим: BRABO_BOT_TOKEN
истёк вечером 2026-09-03, провалил два запуска tag-release и был
ротирован 2026-09-04; v4.0.0 проштамповала и ЗАПУСТИЛА Release, что происходит
только с действительным PAT (тег, созданный с GITHUB_TOKEN, не запускает workflow).
Правило начинает покрывать dev УЗКО: дефектом является только PR, принёсший
НОВОЕ РЕБРО (head был merge, чей второй родитель не был в базе), никогда
squash рабочего PR, что является соглашением. Отключение squash в репозитории было
ИЗМЕРЕНО и отклонено (обложило бы каждый PR и затронуло бы changelog.mjs, который запускается
с --no-merges)
Артефакт решения разговорных агентов (Фронт 3, общий срез) (RN-505)
decision_record переиспользует ОБЩИЙ шаблон emit_artifact/ArtifactSchemas
(тот же, что у note/business_rule) вместо выделенного шаблона
project_image/c4_diagram — решение — это append-only лог, никогда не
«действующее», которое заменяется. PO, Arquiteto, Dev Lead, UX Designer и Staff
получают инструмент (alias + tool_specs + run_tool/3 + та же фраза в
system_prompt/1); у Criativo emit_artifact уже был с Фазы 3b, и он лишь
получил новый тип, без изменений в нём. Отличается от open_adr_pr (только
Arquiteto, настоящий commit + PR + человеческое одобрение): оба СОСУЩЕСТВУЮТ для
разных масштабов решений. Вне объёма, объявлено: агенты выполнения
(ToolLoop, без общего harness) и связка с Infra Lead — выдача
decision_record при предложении container_start_via_runner, — которая зависит от
инструмента другого фронта того же плана, в отдельной ветке
«Sempre permitir», разделённое по Dev Agent модуля (RN-509)
Клик «sempre permitir» на действии dev-<modulo> записывал в
permissions.json, область ВСЕГО ПРОЕКТА — освобождая ту же команду для
любого другого агента. Теперь записывает в agent_autonomy(projeto, agente, tipo), ТОТ ЖЕ механизм, что уже используется для 3 git-действий, засеваемых при активации
выполнения. dev-lead (возглавляет область, но не является её участником) и любой не-модульный
актор продолжают идти в привычный permissions.json; два абсолютных
предела (терминал с внешним эффектом/привилегированная команда,
container_remove) по-прежнему отклоняют весь клик ДО новой ветки.
Без миграции: старые записи остаются в permissions.json, осознанное
решение
Docker становится настоящей предпосылкой режима Runner (RN-507/508, ADR 0145)
runner маршрутизировал каждый одобренный терминал в CLI, как только проверенный workspace
- подключённый runner совпадали, без проверки контейнера
running— без запущенного Docker runner сам падал в чистый ХОСТ, ТОТ ЖЕ молчаливый откат, который ADR 0143 уже закрыл дляcontainer/mounted.Engine.Runners.RunnerReadinessобъединяет ТРИ предусловия (проверен, подключён, контейнерrunning) с двумя потребителями:TerminalExecutorи материализацией worktree dev agent, которая перестаёт пытаться делать ЛОКАЛЬНЫЕFile.mkdir_p!/git initпротив пути ХОСТА (пробел, открытый с RN-478) и начинает работать ВНУТРИ настоящего контейнера, на машине пользователя, через ТОТ ЖЕ каналexec/exec_result(Engine.Actions.Workspace.RunnerGit, новый) —WorktreeManagerразветвляется по тому же критерию, а периодический job очистки молча ПРОПУСКАЕТ проектrunnerбез готового сейчас runner-а. Протоколexecполучает опциональныйenv, чтобы учётные данные git (ADR 0056) передавались в окружении дочернего процесса ХОСТА — слитые поверхprocess.env, никогда не передаваемые вdocker execи не логируемые.container_startперестаёт обслуживатьrunner(его payload, с выбором образа, там никогда не имел смысла); рождаетсяcontainer_start_via_runner— схема только сrationale, поднимает уже решённый образ, — а новый инструмент Infra Lead ЛОКАЛЬНО отказывает (без HTTP), вместо того чтобы предлагать вслепую
RN-505, задублированная в dev, исправлена
«Артефакт решения» и «sempre permitir по Dev Agent» смержились с разницей в 2
минуты, каждый выделив RN-505, не видя другого; «sempre
permitir по Dev Agent» перенумерован в RN-509 (docs/business-rules.md,
CHANGELOG, комментарий approve-always-action.use-case.ts и
ApprovalCard.test.tsx), освободив якорь {#rn-505} для
decision_record. Найдено заодно: перенумерация 505→507/506→508, сделанная во время
разрешения конфликта другого PR (Docker/runner, ADR 0145), осталась
НЕПОЛНОЙ — в этом самом файле оставалось три упоминания «RN-505», тоже
исправленных на «RN-507»
ФАЗА 28 (сессия 5) — join локального агента перестаёт быть немым (RN-514, ADR 0147, пункт 1)
join канала terminal:<projectId> был немым с ОБЕИХ сторон — runner
отправлял {}, а сервер делал _params, — поэтому у сервера не было способа
узнать, понимает ли бинарник на другой стороне новое сообщение, и дефект был
наихудшим из возможных: сообщение приходит, обработчика нет, и НИЧЕГО не происходит, без
ошибки и без лога. Теперь runner ОБЪЯВЛЯЕТ в params
(Engine.Runners.Capacidades, словарь exec/pty/espelho), а
сервер ПРЕДОСТАВЛЯЕТ пересечение с тем, что ЗНАЕТ, в socket.assigns и НИКОГДА
в таблице (способность — свойство этого ПОДКЛЮЧЕНИЯ). Runner объявляет только
exec и pty — объявить espelho, не реализовав его, — ровно тот дефект,
который предотвращает согласование; он входит как ИМЯ словаря и не более, а
сессия 6 его реализует. Отсутствующие/пустые params — это УСТАРЕВШИЙ бинарник, и они предоставляют
{exec, pty} — ФАКТ, а не благосклонность: это две функции, с которыми runner
родился, а отклонение устаревшего было бы breaking/ и MAJOR. Неизвестное ИГНОРИРУЕТСЯ
(runner НОВЕЕ engine подключается); требуемое-но-не-объявленное отклоняет join,
НАЗЫВАЯ недостающее, ДО Registry — механизм реализован и протестирован,
но сегодня НЕ срабатывает (runner требует exec, который есть у каждого бинарника; ничто пока не требует
espelho), и таков замысел. Половина, приносящая пользу СЕГОДНЯ:
сообщение без способности ОТКЛОНЯЕТСЯ с именованным ответом — exec отвечает в
формате exec_result (126 + причина), вместо того чтобы RunnerRouter ждал
timeout, а pty_* становится pty_error во вкладке вместо исчезновения. RunnerReadiness
байт в байт как есть
ФАЗА 28 (сессия 6, половина B) — зеркало начинает существовать (RN-516, ADR 0147, пункты 2/3/4/8)
Способность espelho выходит из имени словаря и становится кодом с ОБЕИХ
сторон. НАЗНАЧЕНИЕ передаётся в ПРЕДОСТАВЛЕНИИ join (ответ перестаёт быть пустым
для :runner: %{espelho: %{destino: ...}}), живёт только в этом подключении, и
runner ОТКЛОНЯЕТ mirror_sync для непредоставленного назначения — никогда глобальная
конфигурация или переменная, из-за которой артефакт проекта B приземлился бы в папке
A. Непустой mirror_path начинает ТРЕБОВАТЬ способность, и это ПЕРВОЕ
настоящее срабатывание механизма отказа RN-514: старый бинарник в проекте с
назначением ПЕРЕСТАЁТ подключаться, именованно и фатально — opt-in, цена объявлена в ADR.
espelho-guard.ts рождается РОДСТВЕННИКОМ guard.ts (переиспользует три helper-а и двойной
проход, письменно наследует оговорку о TOCTOU) и отклоняет три вещи
ADR; находка при реализации: realpathMaisProximo падает к предку, а поскольку
назначение почти никогда не существует, схлопывание обеих сторон превратило бы /base-outra и
/base в / — второй проход разрешает предка и ВОЗВРАЩАЕТ
суффикс. ОДНО направление и НИКОГДА не удаляет: файл, удалённый в источнике, ОСТАЁТСЯ в
назначении (папка пользователя — накопление, а не реплика). Копируется СПИСОК
GIT (отслеживаемые + неотслеживаемые-неигнорируемые), без собственного списка
исключений; падающий git становится ИМЕНОВАННОЙ ошибкой, никогда cp -r как план Б; вложенный
git-репозиторий (worktree dev agent) возвращается как ОДНА запись и не обходится вглубь, а
symlink источника пропускается. СОБСТВЕННЫЙ предикат (Engine.Runners.Espelho), ДВА
предусловия и НИКОГДА RunnerReadiness с флагом — runner_readiness.ex остаётся
байт в байт. Триггер: COMMIT, именованный момент, fire-and-forget и никогда не
watcher; «конец хода агента» остаётся ОБЪЯВЛЕННЫМ и не подключённым, поскольку нет
однозначной зацепки
Broker, который никогда не поднимался (исправление RN-512) (RN-512, ADR 0130/0146)
RN-512 убрала broker из profiles, чтобы он был запущен в dev, и он НИКОГДА не
поднимался: corepack prepare выполнялся от ROOT при build (кеш в /root/.cache),
контейнер работает как uid 1000, а без кеша corepack пытается СКАЧАТЬ pnpm в
сервисе, чья единственная сеть — internal: true, — EAI_AGAIN registry.npmjs.org,
Exited (1) через пять секунд. Без healthcheck up --wait говорил
Healthy, а полный сброс объявлял «reset completo» с мёртвым процессом;
пока существовал profile, broker поднимал тот, кто уже на него смотрел.
Исправление убирает registry из RUNTIME и НЕ открывает сеть (первый из пяти
слоёв ADR 0130): COREPACK_HOME, подготовленный при build с владельцем uid
runtime, pnpm install как шаг BUILD, и — поскольку три тома
node_modules перекрывают то, что установил образ, а Docker заполняет только ПУСТОЙ
том, — entrypoint, сверяющий их с копией вне /workspace,
помеченной sha256 lockfile из build (та же метка ПРЕДУПРЕЖДАЕТ, что образ
отстал, не отказываясь стартовать). Healthcheck в сервисе, изнутри
контейнера. Production от этого не страдал и не меняется
ФАЗА 28 (сессия 7) — локальный агент становится пользовательской службой (RN-518, ADR 0147, пункт 5)
brabo-runner service install|uninstall|status, и прецедент был НУЛЕВЫМ
(ни одного упоминания systemd/launchd в apps/runner/; моделью был
foreground, убиваемый SIGINT). Всегда уровень ПОЛЬЗОВАТЕЛЯ — systemd --user/LaunchAgent, — а root — это ОТКАЗ, а не предупреждение: docblock guard.ts
объявляет «работает с ЕГО привилегиями» предпосылкой трёх настоящих
границ, а root-служба оставила бы best-effort защиту cwd единственной
преградой между одобренной командой и диском. Windows — ИМЕНОВАННЫЙ отказ (третий
механизм, а не вариация двух). Один unit на ПРОЕКТ, потому что сервер уже
навязывает один runner на проект — именование по папке создало бы два unit-а, которые никогда
не поднимутся вместе, и второй проявился бы как сбой подключения, а не как ошибка
установки; в Linux файл идёт туда, где systemd ИЩЕТ
($XDG_CONFIG_HOME/systemd/user, иначе ~/.config). Restart=on-abnormal и
НИКОГДА on-failure (в plist — KeepAlive/Crashed): exit 1 runner-а — это
фатальный отказ join (RN-514) или исчерпанный предел попыток, и перезапуск
был бы циклом, от которого CLI отказывается сам. Служба аутентифицируется КЛЮЧОМ
УСТРОЙСТВА и никогда токеном — запись его в unit оставила бы его на диске, а этот
CLI никогда не записывал учётные данные на диск. uninstall удаляет ВСЕ ТРИ, читая папку
из UNIT-а и никогда из cwd (без unit — только с --dir; угадывание стёрло бы
ключ другого проекта), и ГОВОРИТ, что ключ ушёл с диска и НЕ был отозван на
сервере. status отвечает ЧЕТЫРЬМЯ состояниями с собственной фразой и кодом
(0/3/4/5), и первый ответ приходит с ДИСКА — поэтому он остаётся верным на
машине без менеджера; неудавшийся spawn никогда не становится «менеджер сказал
нет». BRB-031 здесь НЕ закрывается
Spec runner-а уходила без projectId, и этот путь НИКОГДА не поднимал контейнер (RN-508, ADR 0145)
container_start_via_runner (RN-508, ADR 0145) заканчивался failed в 100%
случаев — «especificação de container recusada em projectId: esperava texto
não vazio, recebi undefined», — а в реальном выполнении exp004 было видно
СЛЕДСТВИЕ: десять dev-tasks без единого агента, который бы их забрал. Эта блокировка
была ВЕРНОЙ (RN-507 и RN-502/ADR 0143 отказывают без ЗАРЕГИСТРИРОВАННОГО контейнера
running); неверным был предыдущий шаг.
EspecificacaoDeContainerParaRunner родилась с ДЕВЯТЬЮ полями, а
especificacaoValidada (packages/docker-port) всегда требовала ДЕСЯТЬ — данные
были под рукой в SpecDeContainer.projectId и просто не копировались; engine
передаёт карту НЕПРОЗРАЧНО, поэтому посередине ничего не терялось. Для контраста,
BROKER всегда работал, потому что он ПОЛУЧАЕТ spec из api и получает
весь SpecDeContainer — именно поэтому ломался только режим runner. Три
зелёных набора тестов не поймали это, потому что каждое звено собирало собственный fixture: никто не
тестировал ЦЕПОЧКУ. Она входит — payload, который составляет НАСТОЯЩИЙ вариант использования,
проходящий через НАСТОЯЩИЙ валидатор, и закреплённое МНОЖЕСТВО полей (raizDoProjeto
— единственное, которое api не отправляет), — в набор тестов api, через ОТНОСИТЕЛЬНЫЙ import, единственное
место, где обе стороны существуют вживую; инвариант
api-nao-consome-docker-port остаётся зелёным и был РАСШИРЕН, чтобы проваливать относительный
import из src/. Родственники проверены и ВЕРНЫ:
container_stop/container_remove отправляют только workspaceDirName, а это всё,
что требует nomeDeWorkspaceValidado
Пауза, которую нельзя было снять (RN-540)
Anamnese и Psicólogo были на ПАУЗЕ с 2026-08-10, и три места называли
паузу обратимой — docblock-и
AnamneseSchedulerWorker.enabled?/0/PsychologistWorker.enabled?/0 («Ligar
de volta é X=true e reiniciar o engine») и
docs/reference/configuration.md. Она не была обратимой НИ в одной среде:
environment: сервиса engine отображал ТРИНАДЦАТЬ переменных
Anamnese/Psicólogo — все — ПРЕДЕЛЫ стоимости — и ни одной из двух ПРОДУКТОВЫХ, а
docker compose НЕ передаёт окружение хоста, поэтому ANAMNESE_ENABLED=true
в .env был инертен, и runtime.exs МОЛЧА падал на значение по умолчанию "false".
Измерено в контейнере dev: [true] [] []. Вероятное происхождение: когда значение по умолчанию
стало false, никому не нужна была переменная, чтобы ВЫКЛЮЧИТЬ, — лишь тот, кто пытается
ВКЛЮЧИТЬ, обнаруживает, что провода нет, месяцы спустя. ВОСЕМЬ булевых флагов
(System.get_env("X", "true"|"false") == "true") входят в ОБА compose с
ТЕМ ЖЕ значением по умолчанию, что в коде, — четыре дополнительных (START_MODEL_SYNC,
START_GATE_RESCUE, два GRAPH_*), потому что это тот же дефект, найденный
самим механизмом, а исключение их потребовало бы четырёх оправданий. Ноль изменений
поведения: продуктовое решение держать оба выключенными остаётся байт
в байт, появляется ПЕРЕКЛЮЧАТЕЛЬ. Механизм —
scripts/ci/flags-do-engine-no-compose.spec.ts, который ВЫВОДИТ список из
runtime.exs (остальные 50 переменных — пределы с хорошим значением по умолчанию и вещи
релиза, законно отсутствующие) и требует одинакового значения по умолчанию только в compose DEV —
production намеренно выключает фоновых агентов. deploy/k8s/ НЕ
меняется, по согласованности: там нет слоя, который ломается
ФАЗА 29 (сессия 1) — установка одной строкой, только планирование (ADR 0149-0152, fase-29-instalacao-de-uma-linha.md)
Установка Brabo — это ТРИ разрозненных жеста (compose с пятью секретами вручную,
runner через браузер с chmod +x, pnpm bootstrap, который существует только в
checkout-е), и ФАЗА 28 письменно отказалась от однострочного установщика, пока
артефакты не подписаны (BRB-005), — поэтому фаза подписывает ДО
установки. Четыре ADR Proposed: подпись cosign keyless по DIGEST плюс
ОДИН подписанный checksums.txt, никогда пять подписей (0149); установщик,
вызываемый через sh -c "$(curl …)" и НИКОГДА curl|sh, потому что с pipe скрипт
ЯВЛЯЕТСЯ stdin, и согласие умирает (0150); согласованная база runner-а,
переиспользующая dentroDoEscopo и отказ от петли по СЕГМЕНТУ из
espelho-guard.ts вместо четвёртой копии линейки (0151); и
бэкап/restore против compose (0152). Измерение исправило ПЯТЬ пунктов
исходного среза: workspace.verified НЕ существует (существует
workspace_confirm, и он ОДНОНАПРАВЛЕННЫЙ), пункт 5 ADR 0146 версионирует
consentir-base.mjs, а не install.sh, docker-compose.prod.yml не
потребляет GHCR и объявляет себя compose ПРОВЕРКИ, make test-restore — путь
Kubernetes с бэкапом только Postgres, а «один runner на проект»
навязан в ПЯТИ независимых местах — что вывело runner на машину
из этой фазы в ФАЗУ 30. Собственная находка: git_local_repos не входит
ни в один бэкап, и потерять его — значит потерять код проектов local. Рождается
docs/reference/brb.md — и находка в том, что реестр BRB не принадлежит
репозиторию: он живёт в vault мейнтейнера, с 33 пунктами, а здесь в прозе
появлялись лишь два id. Измерение: BRB-032 и BRB-033 описывают дефекты, которые больше не
воспроизводятся, а BRB-004 — не то, что подсказывает id: он просит сторонний образ
по DIGEST, а не тег, который закрепил #419. Здешний файл хранит УЛИКИ
(измеренное состояние, arquivo:linha); vault остаётся владельцем находки. Диапазон
RN-524..538 и ADR 0149-0152, пропуская то, что ВЕДЁТСЯ: три коллизии,
замеченные в один момент (ADR 0148 ×2, RN-522 ×2, RN-523 ×2), разрешены
позже по критерию ДАТЫ — кто выделил первым, сохранил номер, а
ветки от 08.09 стали ADR 0153, RN-539 и RN-540
ФАЗА 29 (сессия 2) — конвейер подписывает то, что публикует (RN-524, ADR 0149)
Продукт не подписывал ничего: cosign/sigstore/attestation не существовали в
.github/, scripts/ или docker/, а единственным происхождением был
--provenance npm-пакета runner-а. release.yml публиковал четыре
образа без id-token: write; build-runner-binaries.yml загружал пять
бинарников через gh release upload и ничего больше — тогда как уже существовавшие
sha256sum -c проверяют сторонние бинарники, шаблон, который репозиторий
умел применять и никогда не применял к самому себе. Теперь образы подписываются по
DIGEST (никогда по тегу, подвижному указателю), а бинарники получают ОДИН
подписанный checksums.txt, в job-е checksums, который ждёт матрицу, — а не пять
подписей, потому что тот, кто проверяет четыре и забывает пятую, не знает, что
забыл. Job намеренно if: always() (fail-fast: false в матрице) и
называет то, что манифест не покрывает. Оба workflow проверяют перед
публикацией. Keyless по истории дома: приватный ключ был бы ещё одним
секретом CI, а PAT уже однажды молча истёк (ADR 0139). Процедура
проверки — в runbook, с двумя обязательными флагами --certificate-* —
без них cosign принимает действительную подпись ОТ КОГО УГОДНО. Закрывает BRB-005
для образов и бинарников; вне остаются, объявлено, code-signing ОС и
прокси загрузки
ФАЗА 29 (сессия 3) — установщик, который ещё не устанавливает (RN-526, ADR 0150)
Рождается install.sh в КОРНЕ, версионируемый и раздаваемый через Release. Форма — sh -c "$(curl …)" и никогда curl|sh, по МЕХАНИЧЕСКОЙ причине: с pipe
stdin процесса ЯВЛЯЕТСЯ загрузкой, и каждый read читает байты самого скрипта или
находит EOF — установщик, который не спрашивает, сам выбирал бы, где создавать папку
на чьей-то машине. Он ПРОВЕРЯЕТ СОБСТВЕННОЕ ПРОИСХОЖДЕНИЕ (подпись
checksums.txt из RN-524 и свой хеш ВНУТРИ проверенного манифеста), с
закреплённым cosign, сверяемым через sha256sum с хешем, записанным в скрипте;
ОБНАРУЖЕНИЕ называет найденное, прежде чем спрашивать; без TTY сообщает и выходит с 0; и
никогда не удаляет папку пользователя — база проектов и папка зеркала остаются
вне любого подтверждения, по линейке зеркала (RN-516). Маркер в
$XDG_STATE_HOME/brabo/, никогда в ~/.brabo/, который ПО ПРОЕКТУ. В этой версии
ничего не устанавливает, и ГОВОРИТ об этом, вместо того чтобы молча закончить.
--print-plan/--print-state раскрывают решение без TTY, без сети и без эффекта
— по образцу --print-commands из bootstrap.sh, тестируемые так же. Два бага
bash, найденные при запуске, оба одного рода: под set -e+pipefail, [ cond ] && atribuição, дающее ложь, УБИВАЕТ скрипт, а несовпавший ls
роняет всю подстановку — в функции, чей нормальный случай — «ничего
не нашёл». КОРЕНЬ репозитория не покрывался ни одним docmap (glob-ы
корня — буквальные имена); рождается правило instalador
ФАЗА 29 (сессия 4) — установка поднимается из собственного compose (RN-527, ADR 0150)
Рождается docker/docker-compose.install.yml, СГЕНЕРИРОВАННЫЙ из production-а и
отличающийся в трёх пунктах: четыре образа берутся из обязательной переменной
(${BRABO_API_IMAGE:?…} — со значением по умолчанию отсутствие переменной подняло бы половину
стека с образом, который никто не выбирал, и ошибка стала бы странным поведением,
а не отказом), ни один build: не выживает, и сервиса broker НЕ
существует, потому что его образ не публикуется (у bake четыре цели) — с
--source=ghcr mounted не поднимает контейнер, и compose ГОВОРИТ это в
комментарии к BROKER_URL. docker-compose.prod.yml остаётся нетронутым: он
объявляет себя compose ПРОВЕРКИ и используется smoke.sh с локальной сборкой.
Цена двух файлов объявлена, а предел МЕХАНИЧЕСКИЙ —
composes-em-conformidade.spec.ts проваливает лишний сервис/том/порт,
выживший build: и ослабленный сторонний образ. Две вещи из плана
умерли при контакте с диском, по противоположным причинам: migrate — не шаг
(compose связывает api→migrate-api через service_completed_successfully,
и два места, велящих мигрировать, были бы двумя источниками одной истины), и **
smoke.sh не переиспользуется** (он СОБИРАЕТ то, что установка скачала по digest,
и роняет стек в конце) — вместо этого установщик СПРАШИВАЕТ, прежде чем утверждать,
стуча в /health обоих
ФАЗА 29 (сессия 6) — миграция требует ДОКАЗАННОГО бэкапа (RN-530, ADR 0150/0152)
Установка поверх существующей установки не предлагается — либо миграция, либо
остановка: up на томах другой версии — это ущерб, который не предупреждает. Порядок:
бэкап → ДОКАЗАТЬ → спросить → удалить → установить → восстановить, и средний
шаг — то, что даёт установщику право удалять: бэкап, который никто не
пытался восстановить, — это файл, и время узнать об этом — не после
down -v. Доказательство запускает test-restore-compose.sh (ТЕ ЖЕ три проверки
ADR 0152); упавшие бэкап или доказательство останавливают миграцию ничего не удаляя.
Назначение — папка ХОСТА — значение по умолчанию compose — том backup_local,
который down -v удалил бы ВМЕСТЕ с тем, что хотят сохранить. Дамп Postgres
НЕ восстанавливается сам поверх заполненной базы (это разрушительно, а brabo-restore
никогда не трогает источник); локальные git-репозитории — да, потому что они родились бы пустыми
ФАЗА 29 (сессия 7) — одна база для обеих сторон, и BRB-031 закрывается (RN-531, ADR 0150/0151)
Установщик спрашивает базу ОДИН раз и записывает её в ОБА места:
BRABO_PROJECTS_BASE в .env (сервер, ADR 0141) и поле base в
runner.json (локальный агент, RN-529) — между mounted и runner различается
то, КТО выполняет, а не где живёт код, и две базы для одной папки были бы
коллизией, которую отклонил ADR 0141. Отказ в ОБОИХ направлениях против checkout-а,
и ~ намеренно не раскрывается (значение идёт в файл конфигурации,
где ~ читается буквально). Бинарник runner-а сверяется с
тем же checksums.txt, который скрипт уже проверил, чтобы проверить себя, —
повторная его загрузка дала бы два шанса получить разные манифесты, и
второй не был бы проверен, — и устанавливается через install -m 0755. BRB-031
закрывается здесь: он пережил ФАЗУ 28, потому что установка не шла из
версионируемого артефакта, а тот, кто доходил до service install, уже нуждался в
chmod. Release без бинарника платформы НЕ прерывает установку
ФАЗА 29 (сессии 8-10) — протокол, picker и E2E (RN-532..534, ADR 0149/0150/0151)
8 (RN-532): рождается пара workspace_create/_result по образцу
exec/exec_result, а подтверждение ПЕРЕИСПОЛЬЗУЕТ уже существующий workspace_confirm
— ни одного нового маршрута записи, engine по-прежнему не пишет таблицу.
Предикат СОБСТВЕННЫЙ по более сильной причине, чем у зеркала: там
третье предусловие было бы циклическим — у проекта runner есть контейнер
running только после того, как runner его поднимет, а контейнер поднимается ПОВЕРХ папки, которую
создаёт это сообщение. Способность workspace — единственная, чьё объявление зависит
от СОСТОЯНИЯ (согласованная база), а не от версии бинарника. 9 (RN-533):
picker снова читает диск пользователя через fs_list_dir, а решение НЕ
скрывать режим без runner-а симметрично RN-513, а не противоположно — там у api ЕСТЬ
сигнал, и без базы создание заканчивается 400; здесь нет сигнала присутствия (
RN-521 уже это объявляет), и скрытие означало бы решить «нет» из «не знаю».
Находка: экран принимал решение, сопоставляя ФРАЗУ 'Nenhum runner conectado', которая живёт
в .ex engine, — она стала различённой причиной. 10 (RN-534):
install.sh входит в подписанный checksums.txt (без этого проверка, которую он делает
САМОГО СЕБЯ, была мёртвой буквой) и проверяется на чистой машине, с TTY,
имитируемым через script -qec, и никогда через pipe — pipe — то, что убивает
согласие. НЕ запускается в PR, по той же причине, что и подпись
ФАЗА 29 (сессия 8) — база локального агента получает потребителя (RN-532, ADR 0151, пункты 3–6)
RN-529 построила БАЗУ и письменно объявила, что её ничто не потребляет. Входит
пара workspace_create/workspace_create_result, по образцу ЗАПРОСА С
ОТВЕТОМ exec/exec_result — и НИКОГДА по образцу workspace_confirm, который
ОДНОНАПРАВЛЕННЫЙ ({:noreply, socket}, ничего не отправляет обратно), уточнение, которое
фаза уже записала вместе с «workspace.verified НЕ существует». Только
относительный СЕГМЕНТ пересекает сеть (инвариант ADR 0130/0144: тот, у кого есть
корень, тот и выполняет), а АБСОЛЮТНЫЙ сегмент отклоняется лексически. Подтверждение
ПЕРЕИСПОЛЬЗУЕТ уже существовавший workspace_confirm, и именно это экономит целый
фронт: не рождается ни одного нового маршрута записи, engine по-прежнему не
пишет таблицу, а единственный путь, штампующий workspace_verified_at,
остаётся единственным — с ПОРЯДКОМ как механизмом (confirm ДО результата,
потому что оба приходят в один процесс канала по порядку, а обработчик
confirm синхронный). Способность workspace входит в словарь В ТОЙ СЕССИИ,
В КОТОРОЙ ВХОДИТ КОД, с обеих сторон — RN-514 сделала противоположную ставку с
espelho, и она окупилась лишь потому, что кто-то её востребовал, — и это ЕДИНСТВЕННАЯ из
четырёх, чьё объявление зависит от СОСТОЯНИЯ данного выполнения: runner
объявляет её только при согласованной базе, и именно по этой строке и ни по какой другой
сервер ЗНАЕТ, что база существует (engine не читает диск пользователя). НИКТО её
не требует в join, намеренно: требование её в runner было бы breaking/ и MAJOR
ради функции, которую этот проект, возможно, никогда не использует. СОБСТВЕННЫЙ предикат
(Engine.Runners.PastaDoProjeto), ДВА предусловия, RunnerReadiness байт
в байт — и здесь третье было бы хуже, чем ненужным, оно было бы ЦИКЛИЧЕСКИМ:
проект runner доходит до контейнера running только после того, как runner его
поднимет, а контейнер поднимается поверх папки, для создания которой существует это сообщение.
Он также не спрашивает workspace_verified_at (здесь это РЕЗУЛЬТАТ) и
режим. ДВА предусловия отвечаются в разных местах, потому что «согласованная
база» — свойство ПОДКЛЮЧЕНИЯ (socket.assigns, никогда таблица): канал
отказывает ДО отправки, с motivo: "sem-base" по тому же ref, в
формате, которого ожидает модуль, — никогда timeout. Пять именованных причин, которые не
схлопываются, идемпотентность с СОБСТВЕННЫМ исходом (ja-era-repositorio — успех),
и estado.dir НЕ меняется. Это не proposed_action, и ни один предел не получает исключения
ФАЗА 29 (сессия 5) — бэкап начинает покрывать то, что больно терять (ADR 0152, RN-528)
Бэкап покрывал ОДНУ базу и работал в ОДНОМ месте: make test-restore — путь
Kubernetes (требует kubectl, кластер и CronJob), а в docker-compose.prod.yml
не было НИКАКОГО сервиса backup — у миграции установки через compose не было
доказанного пути. Решение начинается с ОТКАЗА от лёгкого хода («бэкап
всех томов») и с КЛАССИФИКАЦИИ семи: neo4j_data и
project_workspaces — производные, ollama_data — получаемый заново,
brabo_projects_base — пользовательский: их копирование создаёт впечатление покрытия, не
добавляя восстановления. Источников истины ДВА, и находка — второй:
git_local_repos не входил ни в один бэкап, а проект с provider-ом
local хранит там bare — он не восстанавливается из Postgres (event log
хранит ПОВЕСТВОВАНИЕ, а не объекты git), так что потерять том — значит потерять
код агентов. Проза runbook утверждала обратное («the real
repositories live in GitHub/GitLab»), и именно она объясняет годы без
покрытия: это верно для github/gitlab, а для local upstream нет.
Гарантия архива ОБЪЯВЛЕНА и меньше, чем «снимок», — согласованность по
ССЫЛКЕ (git пишет объекты до сдвига ref и меняет ref атомарным
rename), без глобального момента и без git fsck (в образе намеренно нет
git); конкурентный gc становится СБОЕМ, а не частичным архивом, со статусом
tar, захваченным в ФАЙЛ, потому что в POSIX sh статус конвейера — статус
последней команды. Пять переменных S3 перестают быть обязательными и становятся
конфигурацией ОДНОГО назначения (BACKUP_DIR выбирает диск; без неё — S3,
этот вывод и сохраняет CronJob НЕТРОНУТЫМ). make test-restore-compose
запускает НАСТОЯЩИЙ бэкап и вызывает ТОТ ЖЕ brabo-restore с ТЕМИ ЖЕ тремя
проверками: меняется ОБЁРТКА, никогда суждение, и
deploy/k8s/test-restore.sh остаётся НЕТРОНУТЫМ (объединение сделало бы вариант compose
зависимым от kubectl); restore.sh по-прежнему не трогает ИСХОДНУЮ базу.
Восстановление репозиториев — РОДСТВЕННАЯ команда (brabo-restore-git, по умолчанию проверяет,
пишет только с --restaurar), а не фаза другой — два артефакта, два
суждения, и их объединение заставило бы сбой git проваливать проверку event log.
Находка ПРИ ЗАПУСКЕ, которую чтение бы не дало: том разделяется тремя
образами с разными uid и несёт владельца того, кто смонтировал его первым (измерено
1000:1000, режим 0755), тогда как образ работает как uid 70 — извлечение
умирало посередине, оставляя том наполовину; теперь отказывает ДО tar,
называя --user 0:0, а извлечённое от root возвращается владельцу КАТАЛОГА,
иначе том вернулся бы целым и БЕСПОЛЕЗНЫМ (api и engine, non-root, не
могли бы писать в только что восстановленное). Neo4j получает ПЕРЕПРОЕКЦИЮ, а
не бэкап — здесь не построена (BRB-018): мигрированная установка рождается с
ПУСТЫМ графом, названная деградация, а не потеря данных (RAG живёт в pgvector,
внутри дампа)
ФАЗА 30 — локальный агент машины (ADR 0154/0155, RN-543..552, закрывается в сессии 8 с RN-549)
ФАЗА 29 сдала установку одной строкой и ОСТАНОВИЛАСЬ в двух пунктах, которые сам
продукт уже письменно объявлял: локальный агент оставался сопряжённым по
ПРОЕКТУ (кто хотел использовать установленный бинарник, возвращался в браузер, чтобы вставить
команду в терминал), и никто не мог войти — сгенерированный .env не
содержит переменной e-mail, MAIL_TRANSPORT падает в log, а регистрация требует
подтвердить e-mail, так что выходом было выуживать ссылку в docker compose logs api.
Оба решались в одном месте, и именно это сделало из них одну фазу. Находка, которая
определила её РАЗМЕР, пришла из повторного измерения с arquivo:linha утверждения ФАЗЫ
29 о том, что «один runner на проект навязан в пяти местах»: три из
пяти не нужно было менять, потому что топик, socket id и тикет описывают
ПОДКЛЮЧЕНИЕ, а процесс, открывающий N подключений — по одному на проект, — удовлетворяет им байт
в байт, сохраняя отказ второму runner-у в том же проекте, что является настоящей
гарантией, а не случайностью. Ничто в engine не изменилось за всю фазу, и это
сохранило RunnerReadiness, зеркало и workspace_create нетронутыми. Осталось
то, что принадлежит ПРОЦЕССУ: учётные данные (runner_device_keys.project_id становится
nullable, RN-543 — и список начинает возвращать ОБА помеченных вида,
иначе ключ машины был бы невидимым и постоянным — дефект, который закрыла RN-519,
возродившийся) и unit (RN-545, service install --machine, один на
машину, СОСУЩЕСТВУЮЩИЙ с unit-ами по проекту). Два решения противоречили букве
ADR, и оба были ИЗМЕРЕНЫ: дискриминатор unit — флаг --machine, а не
отсутствие --project (resolverProjeto имеет два источника, и обычный путь —
запуск install без флага изнутри папки, которую настроил браузер, — буквально
ADR молча сконвертировал бы установку того, кто уже пользуется продуктом), и
install ОТКАЗЫВАЕТ, когда другой вид уже установлен, без --force, потому что
оба вместе были бы двумя процессами, оспаривающими один проект. Сессия 6
затронула бы три workspace разом и была РАЗДЕЛЕНА на три, причём две
непересекающиеся части вышли раньше, а install.sh — после них, как оркестратор и ничего
больше. Из этого разделения вышел самый интересный пересмотр фазы: при НУЛЕ подключений
агент перестал ЗАВЕРШАТЬСЯ и начал ЖДАТЬ, повторно опрашивая через 15s/30s/60s
(RN-550) — аргумент RN-544 против повторного опроса остаётся целым там, где он
действует (список, вернувшийся МЕНЬШИМ, неоднозначен, а обрыв ЖИВОГО подключения из-за неоднозначности
меняет верное состояние на догадку), а при нуле подключений обрывать нечего;
пал аргумент про exit 0 («активная служба, которая ничего не делает»),
потому что процесс начал что-то делать и ГОВОРИТЬ, что делает. Измерено: Restart=on-abnormal
не смотрит на код выхода, поэтому ни один unit не изменился, а эффект в том, что unit машины
становится действительно active (running). Другая часть (RN-551) закрывает «никто не СОЗДАЁТ
ключ машины» со стороны агента, и решение там — ОТСЕЧЕНИЕ: CLI не
говорит с api, потому что регистрирует установщик с BRABO_SERVICE_TOKEN
— каждая сторона хранит секрет, и ни одна не видит секрета другой. Отсюда вытекает замысел
из двух шагов, а из него — инвариант: kid ЯВЛЯЕТСЯ id записи и существует только
после неё, поэтому create записывает .parcial, который читатель игнорирует, а
finish записывает настоящее имя — файл, который читает runner, никогда не существует без
kid, дефект RN-475, ставший невозможным по построению. Половина api
(RN-552) объявила цену, вместо того чтобы её скрыть: учётные данные — service token,
и утёкший токен начинает мочь ФАБРИКОВАТЬ долговременные учётные данные — поэтому три
сдерживания живут в МАРШРУТЕ, а не в тексте (в теле нет userId; регистрация
ЗАМЕНЯЕТ активные ключи машины владельца в той же транзакции, так что
переустановленная машина проходит, а тысяча ключей невозможна по клаузе, без числа, которое
устаревает; и JWK с d отклоняется по имени). Сессия 6 (RN-547) связала
пять команд цепочкой и закрепила три правила: упавшее среднее звено ничего не откатывает, становится
именованной строкой в финальном блоке, и скрипт ВСЕГДА завершается с 0 (откат потребовал бы удалить
учётную запись и отозвать ключ, а маршрута для этого нет, и не должно быть такого, который
установщик вызывает сам); идемпотентность — это HTTP-КОД, и 409 — ОЖИДАЕМЫЙ
исход, а не сбой, с 000, обрабатываемым отдельно, чтобы «api не поднялась» не
читалось как «api отказала»; а пароль передаётся через STDIN curl, никогда через
argv, потому что /proc/<pid>/cmdline читаем любым пользователем машины.
Находка этой сессии: --help ЛГАЛ месяцами («ESTA VERSÃO NÃO INSTALA
NADA»), потому что печатает заголовок, являющийся комментарием, а правило, запрещающее
говорить о «сессиях фазы», читало только строки кода. Сессия 7 (RN-548) дала
потребителя метке вида, и решение было о ЧЕСТНОСТИ: с ключом и без
живого подключения — самый частый случай — экран говорит две вещи, которые знает, и называет
две, которых не знает (зарегистрированный ключ — не работающий агент, список — от вашей
УЧЁТНОЙ ЗАПИСИ, а не этого браузера), тон accent и никогда success. Сессия 8
(RN-549) — единственная, которая могла доказать всё вместе, и она РАСШИРИЛА workflow, который
ФАЗА 29 уже имела, вместо создания второго. Её центральное решение — СИГНАЛ:
perguntarEstado отображает activating в rodando — обоснованно, — поэтому
systemctl is-active и service status --machine дали бы зелёный процессу,
который умрёт через две секунды; выбранный сигнал — СТРОКА, которую агент
пишет при входе в ожидание, которая существует только после того, как он прочитал ключ,
взял тикет, поговорил с api и получил ПУСТОЙ список, и которая — ЗАПИСАННЫЙ ФАКТ,
а не выборка состояния. Исход доказывается через API, а не через
браузер, потому что субъект утверждения — АГЕНТ, а то, что он наблюдает, —
GET /runner/projects; браузер создал бы проект через тот же маршрут с лишним
слоем, падающим по посторонним причинам, а утверждение по-прежнему было бы
строкой журнала. И E2E остаётся ВНЕ pull_request, со следствием,
объявленным, а не скрытым (PR, который его написал, его не выполнил): открыть
эту дверь — значит дать установщику способ пропустить проверку происхождения, что
отвергает ADR 0150, — отсюда половина, работающая в PR, — это ТЕСТ
(scripts/dev/install-e2e.spec.ts), охраняющий два способа, которыми workflow может
молча сгнить: ослабленный gate и переписанная фраза install.sh (из-за которой
утверждения не падают, а ИСЧЕЗАЮТ). Проявились два дефекта, оба
невидимые, потому что workflow никогда не запускался: ответы TTY начинались с
nao-migrar, что на чистой машине попадает на вопрос МАРКЕРА и заставляло
скрипт выходить с 0, говоря «Nada foi gravado»; и НАХОДКА, оставшаяся находкой, —
опубликованный install.sh ничего сам не поднимает на чистой машине, потому что
docker/docker-compose.install.yml (вместе с ./postgres/init.sql) не является
asset-ом Release, не входит в подписанный checksums.txt, и он его нигде
не скачивает, так что тот, кто следует curl … install.sh из runbook, умирает на «no
such file or directory» ПОСЛЕ того, как уже проверил подпись и записал
.env. Исправление — решение между подписанным asset-ом и clone, и это отдельная сдача.
Что фаза оставила ОБЪЯВЛЕННЫМ и не сделанным: отзыв по-прежнему достигает
{projeto, usuário} и никогда {chave}, и теперь это стоит дороже (роняет агента во
ВСЕХ проектах этой машины); нет экрана, где перечислить и отозвать ключ
устройства, а фаза добавила вид для перечисления, не построив экран, —
пробел стал БОЛЬШЕ, намеренно; один процесс на машину — единая точка
отказа, цена замысла; и install --machine по-прежнему не знает, является ли ключ
этой папки действительно ключом машины, потому что на диске оба вида — один и тот же
файл, а знает это сервер
Golden-set QA снова измеряет и запускается по расписанию, еженедельно (AT-076/AT-149, ADR 0168)
Две сдачи, одно человеческое решение между ними. AT-067 измерила, что
время укладывается в ubuntu-latest без GPU и что инструмент не измерял: начиная с
RN-502 npm test проекта container без контейнера running
отклоняется, а seed никогда его не поднимал — 0/6 по построению. Мейнтейнер выбрал
«путь 1», и AT-076 его сдала: seed поднимает контейнер каждого случая
путём PRODUCTION (module_map → маршрутизация → container_start, предложенный
Infra и одобренный владельцем → ExecuteContainerStartUseCase → broker → running
через машину состояний), с node:24.11.1-bookworm-slim, закреплённым по digest,
нетронутой RN-502 и контейнером, который не поднимается, становящимся ИМЕНОВАННОЙ ошибкой, проваливающей
ExUnit (раньше {:skip, _} из тела молча проходил зелёным). Два раунда в CI
дали 3/6 и 3/6 против пола 1/6, с разными случаями, за 52 и 86 min
job-а. AT-149 закрыла периодичность: ЕЖЕНЕДЕЛЬНО (суббота 01:17 UTC, плюс
workflow_dispatch, никогда pull_request), в golden-set-qa.yml, по образцу
golden-set-rag.yml. Еженедельно, а не еженощно, потому что раунд стоит до 86 min и
МЕНЯЕТСЯ — тот же итог по разным случаям, — и одного в неделю достаточно, чтобы пол
обнаружил падение. timeout-minutes: 150 по худшему измеренному случаю. Вердикт —
вердикт самого теста; workflow добавляет лишь две защиты ИНСТРУМЕНТА (строка
счёта должна существовать, а в event log не может быть отказа RN-502) и, при
сбое, печатает логи, как у RAG, — без issue. Образ для pull до seed
ЧИТАЕТСЯ из константы seed, никогда из второго литерала, потому что docker run
broker-а работает под управляющим пределом 30 s (RN-604). Раунд доказательства, с
временным триггером push, удалённым до PR, был 36327281774: снова 3/6,
ноль отказов RN-502, 10 min mix golden_set.qa и 13 min 41 s
job-а. Объявлено: у broker-а CI нет пяти слоёв compose, heartbeat
закрывает сессии seed, пока QA ещё в них работает (без видимого
эффекта), а PR, затрагивающий workflow, его не выполняет.
Infra Lead разговаривает через composer (AT-141, ADR 0175, RN-617)
RN-584 (AT-098) убрала получателя по умолчанию из чата и оставила продуктовый вопрос
письменно: разговаривает ли Infra Lead? Мейнтейнер ответил да
2026-09-27, с условием, которое поставила сама RN-584, — сначала ход.
Ход Infra Lead выполнялся ЦЕЛИКОМ внутри handle_call/handle_cast: без
принятия, без «Parar», вне TurnoOrfao, с 14-м шагом цикла, заканчивающимся
молча, и пределом 180 s меньше 225 s propose_action контейнера
(RN-605). Kickoff, будучи cast, не держал клик, но держал процесс —
8,4 min измерено в AT-089, — и сообщение в это время ждало бы в очереди.
Этап 1 перевёл три хода (kickoff, исправление gate, сообщение) в
TurnoAssincrono, с консолидацией Workflows внутри Task; исправление
gate, пришедшее при идущем ходе, получило очередь, потому что TurnoAssincrono
без from его отбросил бы. Этап 2 дал infra клаузу message/2
(ушёл отказ agente_sem_conversa), via_for для «Parar» и место в
AGENTES_DE_CHAT; принятие его handoff осталось в собственной карточке RN-499, а
карточка нити исключает его по имени. SOLO_CONVERSATIONAL_AGENTS не изменился: Infra
Lead — lead области. Объявлено: он не получил ни структурированных вопросов, ни
чтения backlog.
Аутентифицированный git выполняется на хосте runner-а, код — в контейнере (AT-116, ADR 0193, RN-676)
2026-10-01, раунд 36. RN-558 закрыла половину МОЛЧАНИЯ — аутентифицированный git fetch в режиме runner, при запущенном контейнере, перестал выполняться с
пустыми переменными и стал отклоняться с меткой
credencial-nao-atravessa-o-container, — и объявила другую половину незакрытым
ADR, потому что каждый известный вариант затрагивал порт сдерживания ADR 0130.
Решение владельца (01.10) выбрало тот, что не затрагивает: git-операция с учётными данными
на ХОСТЕ, код в контейнере. Измерено заранее: единственный exec engine с env
был fetch из RunnerGit.fetch!/3; не было ни push, ни аутентифицированного clone
через exec (clone при создании папки уже выполнялся на хосте, RN-532); и папка —
одна и та же с обеих сторон (estado.dir, смонтированная в /work). Выбранный
дискриминатор — явная МЕТКА engine (gitCredenciado: true, ставится только
через RunnerRouter.exec_git_credenciado/5), а не уже существовавший env —
с env в роли ключа любая команда с env ускользнула бы из контейнера. Отказ
RN-558 выжил в уменьшенном виде, для env без метки, и на практике
стал означать «runner старше ADR 0193». Доказательство — AT-111, с обеих
сторон: в runner-е настоящие git credential fill и git fetch origin с
helper-ом GitAuth проходят на хосте при активном контейнере; в engine цепочка
через настоящий TerminalChannel заканчивается {:ok, _}. ExUnit не запускались в
среде сдачи (repo.hex.pm 403) и остались для CI.
Консультативная сессия без агента просит агента (AT-254, RN-682)
2026-10-01, раунд 36. В реальном использовании 29.09 консультативная сессия без агента
ответила «у меня нет доступа к предыдущим разговорам»: сообщение без
получателя шло в SSE POST .../chat, который отправляет привязанной модели только
текущий текст — без истории, без системного промпта — и записывает ответ с
именем модели в качестве актора. Решение владельца (01.10) — просить агента, а не
давать историю свободному чату. Измерено заранее: экран был единственным клиентом
маршрута (curl, печатаемый seed-ом, — подсказка для разработки, в креативной
сессии), а у варианта использования SendChatMessageUseCase был второй
законный потребитель, smoke-тесты provider-ов, которые используют его как сквозной инструмент
в сессии без агента. Поэтому отказ api живёт в МАРШРУТЕ, в собственной
защите, а вариант использования остался нетронутым. На экране строка получателя стала
нести собственный селектор ручного handoff, когда агента нет, — тот же
привычный жест, теперь в том месте, где он — единственный способ дать сообщению
назначение, — а Criativo остался вне списка, потому что консультативная сессия обещает, что он
не войдёт.
Отзыв нацелен на ключ, а не на пару {projeto, usuário} (AT-013, ADR 0201, RN-685)
2026-10-01, раунд 36. RN-520 заставила отзыв достигать живого
подключения, с целью {projeto, usuário}, потому что идентичность учётных данных умирала
в PatAuthGuard; ADR 0154 поставил «отзыв по ключу» в список того, чего он не
делал, а PR #534 дал ключу машины ту же пару, применяемую проект за
проектом. AT-013 была заблокирована по объёму и по решению, пока владелец не
выбрал (01.10): по КЛЮЧУ, с ADR, затрагивая engine. Измерено заранее:
тикет рождался из {userId, kind}, сокет хранил только проект/пользователь/kind,
а канал сравнивал пользователя — runner того же пользователя с PAT или другим
ключом падал вместе с ним; подключение ключа машины в проекте вне списка
api оставалось запущенным; тикет, выпущенный до отзыва, входил после него; а
отзыв PAT ничего не ронял. Сдано: два nullable-столбца в тикете
(миграция Ecto, таблица принадлежит engine, — журнал Drizzle остался на 0068),
guard, аннотирующий учётные данные после авторизации, id сокета, получающий
вид и id учётных данных, и маршрут runner/disconnect-credential, который аннулирует
ожидающие тикеты и спрашивает каждый runner кластера, родился ли он из них.
Запрет AT-013 (не перестать ронять то, что падало) стал двумя переходными
частями: устаревшее подключение падает по паре в проектах строки, а engine
без маршрута заставляет api вернуться к паре. Пара осталась целью удаления
участника. Экран сменил лишь текст подтверждения (RN-561). Runner не изменился.
ExUnit не запускались в среде сдачи (repo.hex.pm 403) и остались
для CI.