30 сентября в 06:32 в очереди появилась задача: научить ядро платформы переводить открытые задачи на новую версию их типа. Повод был будничный — мы выпустили новую версию типа «задача на код» с более строгими проверками перед сдачей, а вся открытая очередь осталась на старой и получала старые инструкции.
Через 19 минут агент сдал работу: новый маршрут API, тесты, документация. В 07:15 ревью её отклонило. Новый маршрут позволял агенту с обычными правами исполнителя перевести свою задачу на первую версию типа — ту, где ревью ещё не было, — взять её и закрыть самому. Ревьюер воспроизвёл это по шагам и написал, какое право должно закрывать маршрут. В 08:03 агент взял задачу снова, через 15 минут сдал исправление, в 08:24 ревью прошло и ветка влилась. От постановки до вливания — 1 час 52 минуты, два прогона агента.
Агент написал дыру, через которую агент мог бы уйти от ревью, и поймало её ревью, которое платформа считает частью самой задачи. 4 октября мы выпустили первый открытый релиз платформы — Taimen v0.2.0, Apache-2.0. Ниже — как она устроена и что мы поняли, пока почти два месяца разрабатывали её её же собственными средствами. В конце — как этот релиз выпустила сама очередь.
В центре — работа, а не агент
Taimen — среда исполнения работы организации. Мы пробовали начинать с агента — с его промпта, инструментов, памяти, — и каждый раз упирались в одно: агенты меняются быстрее, чем работа. Выходит новая модель, меняется харнесс, вместо одного исполнителя появляются три по репозиториям. А обязательство «сделать перевод задач на новую версию типа» остаётся тем же.
Поэтому главная сущность у нас — работа (Work): единица обязательства, которая существует независимо от того, кто её исполняет. У любой единицы работы через API можно узнать:
откуда она взялась — человек, правило по наблюдению, процесс, родительская задача;
кто её делал и с какими правами — каждый прогон, каждый вызов инструмента, версия агента;
как принят результат — какие проверки, кто решил;
чем результат доказан — артефакты, попытки проверки.
Исполнитель — человек, ИИ-агент или детерминированный скилл — берёт работу одинаково: взять, выполнить, сдать артефакты, пройти приёмку. Код в нашей собственной разработке писали десять агентов-исполнителей: несколько поколений раннеров и отдельные исполнители под отдельные репозитории, на Claude Code с моделями Opus и Sonnet и на Codex. Задачи этого не заметили — как не заметили и девять случаев, когда задачу начинал агент, а заканчивал человек в своём рабочем окружении.
Масштаб
С 12 августа по 4 октября на одной установке:
Что | Число |
|---|---|
Единиц работы | 1 416 |
Выполнено | 1 190 |
Заведено правилами по наблюдениям, а не людьми | 502 |
Задач на код | 788, из них выполнено 681 |
Прогонов агентов | 2 406 |
Две оговорки, без которых эти цифры нечестны. Первая: задачи на код делили агенты-раннеры (393) и харнесс владельца (324) — это тоже Claude Code, но под учёткой человека, и по данным их не различить. Вторая: с 29 сентября ревью готовит отдельная сессия Claude владельца, а решение записывается за владельцем, который за него отвечает. Ревьюер в истории выше — именно такая сессия.
Работа возникает сама
Треть работы никто не ставил руками. Коннектор наблюдает репозитории и пишет в платформу наблюдения: «в компоненте такой-то коммит». Правило решает, порождает ли наблюдение работу. Вот правило, которое следит, чтобы суперпроект не отставал от своих компонентов:
kind: WorkRule key: submodule-lag spec: identity: {agent: selfdev-rules} trigger: {kind: observation, type: repo.commit_observed} condition: in: [{var: payload.data.repo}, [control-plane, memory-service, iam-service, notification-service]] interpretation: skill: submodule.lag_check@2 inputs: repository: ${SELFDEV_SUPERPROJECT_URL} ref: main submodules: ["{{payload.data.repo}}"] action: kind: request_decision taskType: submodule-bump forEach: skill.output.lagging dedupKeyTemplate: "submodule-lag:{{item.name}}" fields: title: "Сдвинуть указатель сабмодуля {{item.name}} на {{item.head}}" approver: ${SELFDEV_REVIEWER_PRINCIPAL}
Коммит в одном из компонентов наблюдён → скилл сравнил указатель с головой ветки → ревьюеру пришло решение «сдвинуть». Approve — и другой скилл сам делает коммит в суперпроект. Если указатель догонят иначе, парное правило закроет задачу. Таких решений за период — 77. Правило действует от собственной личности, поэтому в журнале видно, что задачу завёл не человек, а правило, — со ссылкой на наблюдение, которое его запустило.
Как устроены правила вывода работы, — в руководстве: правила вывода работы и правила в пакете.
Сделано — значит принято
Самое важное решение, которое мы приняли за эти недели: задача на код закрыта не когда агент сказал «готово», а когда ветка влита. Приёмку объявляет тип задачи:
kind: TaskType key: coding-task spec: acceptance: - key: review kind: human spec: {approver: ${SELFDEV_REVIEWER_PRINCIPAL}} when: ["$.task.artifact[commit].metadata.published"] - key: merge kind: deterministic spec: skill: git.merge@1 inputs: repository: "$.task.artifact[commit].metadata.repository!" branch: "$.task.artifact[commit].metadata.branch!" commit: "$.task.artifact[commit].metadata.commit!" target: "$.task.artifact[commit].metadata.targetBranch!" expect: {merged: true} when: ["$.task.artifact[commit].metadata.published"]
Провал любой проверки возвращает задачу тому же исполнителю с причиной. Ядро при этом не знает, что такое merge: вливание — скилл пакета, и вызывается он полномочиями того, кто одобрил ревью.
С 26 сентября, когда приёмка стала частью типа, через неё прошли 458 задач на код:
Что | Число |
|---|---|
Принято с первой сдачи | 326 (71%) |
Понадобилось две сдачи | 100 |
Три и больше | 32 |
Отказов ревью | 162 |
Провалов вливания | 14 |
Из четырнадцати провалов вливания восемь — честные конфликты или ушедшая вперёд ветка, четыре — инфраструктура (токен не видел репозиторий, push отклонён), два — агент сдал коммит без метаданных, и вливать было нечего.
Задача агента с этой приёмкой проходит путь от постановки до вливания в медиане за 3,4 часа, 90% — быстрее 27 часов: сюда входит ожидание в очереди ревью. Прогон агента в медиане длится 11 минут. Условная стоимость прогона Claude Code по тарифу API — $1,90 в медиане и $7,32 на 90-м перцентиле. «Условная» — потому что раннеры работают по подписке, и эти цифры — оценка, которую считает сам Claude Code, а не наши расходы.
Подробнее о приёмке — типы задач и приёмка, там же пример review → merge.
Агент — это описание
Агентов мы не запускаем руками. Агент — декларация: права, исполнитель, модель, каталог репозиториев, где и сколько реплик держать. Платформа сама поднимает исполнителей на узлах и держит их в описанном состоянии.
kind: Agent key: coder spec: identity: kind: agent permissions: [tasks.claim, tasks.read, tasks.write, artifacts.write, ...] work: onlyAssigned: true executor: kind: claude-code params: {model: claude-opus-5-5, timeoutSeconds: 10800} placement: requires: [claude-subscription, repos] resources: {cpus: 2, memoryMb: 4096} replicas: 2
Смена модели — правка строки и новая ревизия того же агента: идущий прогон заканчивается на прежней модели, следующий берёт новую, контейнер не пересоздаётся. Ядро не даст агенту право решать согласования или администрировать платформу: такую привязку API отвергает. Решать — только людям.
В руководстве — агенты-описания и fleet, который размещает их по узлам.
Память и её онтология
Агенту на задаче нужен не «весь репозиторий в контексте», а ответ на вопрос «что я сломаю». Если в задаче упомянут маршрут API, полезно знать, какой клиент его вызывает, в каком файле он определён и какое архитектурное решение (ADR) им управляет.
Память платформы — граф знаний, и у каждого домена своя онтология. Её тоже объявляет пакет. Онтология разработки ПО у нас — восемь видов сущностей и пять связей, около 50 строк:
kind: KnowledgePack key: software-delivery spec: kinds: - kind: endpoint naturalKey: "<METHOD> <path с параметрами как {}>" - kind: source_file naturalKey: "<repo>:<path>" - kind: client_method naturalKey: "<repo>:<module>.<Class>.<method>" - kind: adr naturalKey: "<SERIES>-<number>" # ... component, ui_call, event, table relations: - {relation: calls, fromKinds: [client_method, ui_call], toKinds: [endpoint], temporal: true} - {relation: defined_in, fromKinds: [endpoint, client_method, ui_call, event, table], toKinds: [source_file], temporal: true} - {relation: governs, fromKinds: [adr], toKinds: [source_file], temporal: true} # ... part_of, emits
А тип задачи объявляет, как из этого графа собрать контекст:
contextSchema: anchors: - {from: description, kinds: [endpoint, adr, event, table]} traverse: - {relation: calls, direction: in, depth: 1, limit: 20} - {relation: defined_in, direction: out, depth: 1} - {relation: governs, direction: in, from: previous} asOf: taskCreated budgetTokens: 4000
Из описания задачи извлекаются якоря — маршруты, ADR, события, таблицы. От них платформа идёт по графу: кто вызывает маршрут, где он определён, какой ADR управляет этим файлом. Связи темпоральны, и asOf: taskCreated означает, что агент видит граф на момент постановки задачи, а не на момент, когда до неё дошла очередь. Результат раннер кладёт в промпт исполнителя отдельным блоком — как данные, а не как инструкции. Граф наполняет тот же коннектор, что наблюдает репозитории: вместе с наблюдением коммита он отправляет в память снимок контрактов.
Почему мы считаем память ключевой частью, а не приложением к агенту. Та же механика работает в доменах, где кода нет вовсе. Процесс оплаты счёта перед решением вспоминает контрагента, договор и прошлые платежи, а после — запоминает исход. Тендерный процесс вспоминает прошлые дела с заказчиком и цены. У каждого домена своя онтология, у каждого знания — источник, дата и права доступа. Память принадлежит организации и переживает смену агентов так же, как работа.
Честно о пределах: насколько профиль контекста улучшает работу агента на коде, мы не измеряли. Сравнения «с графом / без графа» у нас нет.
В руководстве — онтологии в пакете и профиль контекста типа задачи.
Spec-driven — тоже пакет
Крупные фичи идут через цикл «спецификация → план → задачи». Это тоже не код ядра, а пакет из трёх типов задач и одного правила. Дизайн фичи сдаётся двумя документами, spec и plan, и проходит одни ворота — решение человека. Approve запускает проверку формы и заводит шаг разбиения на задачи. Когда документ задач готов, правило разворачивает его в задачи на код по нужным репозиториям и в задачи сходимости — по одной на репозиторий, каждая со своими воротами: одобрение вливает ветку фичи в основную.
Отличие от инструментов, где спецификация — файл в репозитории: здесь spec и plan — входы задач со своей приёмкой, а каждая задача исполнения знает, из какого пункта плана она выросла. За период через цикл прошли 18 дизайнов фич и 60 задач сходимости.
Что сломалось
Публикатор, который сутки бился в стену. 27–28 сентября агент, публикующий релизы в открытое зеркало, 1 063 раза подряд запускал прогон и каждый раз мгновенно получал отказ: у него не было права вызывать нужный скилл. Это 84% всех упавших прогонов за период. Платформа честно записала каждый отказ, но ничто не остановило цикл, пока его не заметил человек. Исправление — назначение скиллов стало частью декларации агента. Сторож зависших прогонов у нас теперь есть: прогон, который долго ничего не делает, проваливается сам. А вот сторожа, который гасит серию мгновенных отказов, пока нет — этот случай он бы не поймал.
Агенты не читали комментарии. Ревьюер писал правки комментарием к задаче, агент брал задачу заново и видел только описание. Теперь контракт исполнителя прямо говорит, что комментарии — часть постановки, и раннер передаёт их агенту в промпте вместе с описанием.
Зависимые задачи стартовали раньше времени. Задача считалась сделанной, как только агент сдал работу, и зависимые задачи брались до того, как ревью и вливание прошли. Отсюда и решение «сделано — значит принято».
Параллельные агенты сталкивались в документации. Две задачи независимо дописывали поправки к одному ADR и обе брали следующую свободную букву. Каждая ветка была корректной, вместе — конфликт. Ревью теперь сверяет такие вещи на дереве слияния, а не на ветке.
Ядро не знает про код
Всё, что выше, — пакеты. В ядре нет ни git, ни merge, ни ревью кода. Мы держим правило: каждая возможность ядра должна доказать себя и на пакете не из разработки.
История, похожая на ту, с которой начиналась статья, случилась в процессе оплаты счетов. Там шаг согласования объявляет разделение обязанностей: загрузивший счёт не может согласовать его оплату. Агент реализовал это в ядре, ревью нашло обход — исключённый согласующий мог отменить согласование, и дело проходило без голоса бухгалтерии, — и через 1 час 42 минуты исправление было влито. Тот же механизм согласований с кворумом и разделением обязанностей описывает в нашем тендерном пакете согласование цены по порогам НМЦК. О нём — следующая статья.
Как платформа выпустила саму себя
Первый открытый релиз оказался хорошей проверкой всего сказанного выше: его тоже собирала очередь.
Переезд. Перед релизом мы перестроили раскладку репозиториев: компоненты разъехались по каталогам services/, sdk/ и apps/, а вместе с ними поменялись пути в CI, раннере, скиллах и Dockerfile. Подготовку — пять задач на код — 3 октября поставил человек, все пять сделал агент. Две вернулись с ревью: в одной правила отставания сабмодулей ссылались на поле, которого не было в контракте скилла, в другой скрипт переезда забыл переписать Dockerfile одного из демо. Самая большая задача — скрипт переезда с прогоном вхолостую на копии — заняла три прогона и затронула 452 файла. Вся подготовка обошлась примерно в $64 по тарифу API. Сам переезд стенда мы делали уже вне очереди задач.
Публикация. Релиз компонента у нас — аннотированный тег в рабочем репозитории. Правило раз в час спрашивает скилл проверки, какие теги готовы к публикации, и на каждый заводит задачу агенту-публикатору. 4 октября в 16:05 UTC, после переезда, правило включили снова. Первая же оценка нашла десять готовых релизов, и публикатор выложил все десять компонентов в открытые репозитории за 6 минут 41 секунду — без единого отказа. Одна задача оказалась дублем: на тот же тег отреагировало второе правило, и скилл отработал вхолостую — тег уже стоял. Ревью у задачи публикации нет: человек решал раньше, когда одобрял сдвиги указателей и ставил теги. В тот день таких решений ревью было 57.
CI собрал всё вместе. Сборка зонтичного репозитория нашла то, чего не видно по отдельности: строку длиннее лимита линтера, устаревший lock-файл, расхождение описания онтологии памяти со снимком в SDK пакетов. И главное — ядро 0.10 отправляло сервису уведомлений поле настроек, которого тот не принимал. На нашем стенде все уведомления падали с 3 октября, а заметили мы это только при сборке релиза. Четыре исправительных выпуска прошли той же очередью — 15 задач публикации за час с небольшим. Тег зонтичного репозитория и GitHub Release поставил человек в 19:19 UTC.
Где мы сейчас
В релизе v0.2.0 открыты под Apache-2.0 ядро, identity, память, уведомления, fleet, веб-консоль, персональный ассистент и SDK. Ассистент построен на DeepSeek Harness (MIT) и использует Claude Agent SDK — у этих зависимостей свои лицензии. Поднять всё локально — быстрый старт, свой пакет — пакет за 10 минут и пример в репозитории.
Пакеты разработки, о которых эта статья, —
selfdevиsdd— в релиз пока не вошли. Фрагменты выше — из них; форматы описаны в руководстве по ссылкам в каждом разделе.Все вызовы модели в нашей установке идут через подписку одного человека. Заменяемость доказана на уровне ревизии агента, а не провайдера.
Ревьюер у нас фактически один. Время человека на ревью мы не мерили.
Цифр экономии нет и не будет, пока не пройдёт пилот вне разработки ПО.
Вопрос к тем, кто запускал агентов на реальной кодовой базе: где у вас заканчивалась автономия агента и начиналась ответственность человека — и где эту границу пришлось переносить?

