Три месяца назад я попросил Claude написать мне Express API. Два эндпоинта, тесты, стандартная скука. Через 40 секунд всё работало. npm test — зелёное. Я закрыл терминал и пошёл спать с ощущением человека, который живёт в будущем.
Через 28 дней я открыл этот проект. npm test — красное. Express обновился. Jest поменял поведение. Lockfile уехал. Код, который писал не я, сломался по причинам, которые не имели отношения к коду.
Я сделал то, что делают все: попросил Claude написать заново. Он написал. Другой код. Другая структура. Другие баги. Тесты прошли. Через две недели сломались снова.
В этот момент я понял, что у меня не проблема с кодом. У меня проблема с тем, как индустрия думает про AI-генерацию.
Гниение
У этой проблемы нет нормального названия. Я буду называть её «гниение» — потому что это ровно то, что происходит.
AI-агент — будь то Claude, Cursor, Copilot, Devin — генерирует код в конкретный момент времени. В этот момент:
Node.js — версия 20.11.0
express — 4.21.2
jest — 29.7.0
Lockfile зафиксирован
Среда чистая
Через месяц всё это сдвигается. Не потому что кто-то что-то сломал. Потому что мир движется. И код, намертво привязанный к состоянию мира на момент генерации, начинает рассыпаться.
Это не баг. Это фундаментальное свойство любого сгенерированного артефакта, у которого нет механизма адаптации.
Представьте фотографию. Она точно фиксирует реальность в момент съёмки. Но она не обновляется, когда человек на ней постарел. AI-код — та же фотография. Идеальная в момент создания. Мёртвая через месяц.
TL;DR: что я в итоге построил
CLI-утилита, которая (по данным лично моих кейсов, не исследование, но повторяется стабильно):
Просит AI составить план создания проекта (один раз)
Выполняет план в изолированной песочнице
Сохраняет его как воспроизводимый артефакт с хэшами и проверками
Через месяц запускает тот же план без AI
Сама определяет, что именно сломалось и почему
В большинстве моих кейсов чинит без вызова LLM
continuum run "express api with tests" # AI планирует, песочница выполняет continuum replay <run_id> # повтор без AI, проверка хэшей continuum replay <run_id> --heal # дрейф? починим
Если совсем коротко: я попытался сделать CI/CD для мира, в котором код пишет AI.
Дальше — как я к этому пришёл и почему всё оказалось сложнее, чем «просто запусти ещё раз». И тут будет одна мысль, вокруг которой крутится всё: хэши — это физика, проверки — это смысл. Их нельзя смешивать.
Что пробовали все (и я в том числе)
Попытка 1: «Просто запусти ещё раз»
Самое очевидное. Если код сломался — попроси AI переписать.
Проблемы:
Стоит денег (каждый вызов API — токены)
Каждый раз другой результат (LLM не детерминирован)
Предыдущие решения потеряны
Никакого аудита: что конкретно изменилось и почему
Это как если бы при каждом баге в продакшне вы не чинили код, а увольняли программиста и нанимали нового. Он напишет что-то работающее. Но это будет другая система.
Попытка 2: «Зафиксируй всё до атома»
Pinned deps. Lockfile. Docker. Nix. Зафиксировать Node, npm, каждую транзитивную зависимость.
Это работает... какое-то время. Но:
Зависимости всё равно нужно обновлять (security patches)
Docker-образ устаревает
Node выпускает минорные релизы
Вы не можете заморозить весь мир
И главное — это борьба с симптомами. Вы не делаете код воспроизводимым. Вы делаете окружение статичным. Это разные вещи.
Попытка 3: «Напиши нормальный CI»
Классический CI/CD. GitHub Actions. Тесты на каждый пуш.
Проблема: CI проверяет, проходят ли тесты. Он не проверяет, воспроизводим ли путь от намерения до результата. CI отвечает на вопрос «код работает?». Он не отвечает на вопрос «почему этот код вообще был написан именно так, и можно ли это повторить?».
Когда AI написал код, история его создания — это промпт в чат-окне, которое вы закрыли. Это не артефакт. Это тепловой шум.
Попытка 4: «Агенты сами разберутся»
AutoGPT, CrewAI, LangGraph и прочее. Дай агенту задачу, он сам починит.
Это работает иногда. Ценой бесконечных циклов, непредсказуемых результатов, и счётов за API, от которых глаз дёргается. AI-агент чинящий AI-код — это рекурсия, у которой нет base case. Он может крутиться часами, пробуя разные подходы, и в итоге выдать что-то работающее, но совершенно не похожее на оригинал.
Момент, когда что-то щёлкнуло
Я сидел и смотрел на два терминала. В одном — оригинальный проект от Claude (сломанный). В другом — тот же промпт, выполненный заново (работающий, но другой).
И подумал: а что если бы я сохранил не код, а решение?
Не файлы. Не промпт. А последовательность действий, которую AI совершил, чтобы из намерения «сделай Express API с тестами» получить работающий проект. Со всеми конкретными версиями. Со всеми командами. Со всей структурой.
То есть буквально: план выполнения.
Как если бы вместо фотографии я сохранил рецепт. Фотография устаревает. Рецепт можно повторить. А если ингредиенты изменились — рецепт можно адаптировать.
Это звучит элементарно. Но в этой элементарности спрятана идея, которая меняет всё остальное.
Что такое Execution Plan (и почему это не скрипт)
Скрипт — это набор команд. npm init, npm install express, echo "..." > index.js.
Execution Plan — это набор действий с метаданными. Каждый шаг знает:
Что он делает (создаёт файл, запускает команду)
Какой у него результат (хэш файла, код возврата)
Насколько он детерминирован (запись файла — гарантированно,
npm install— лучшее усилие)Как его проверять (по хэшу файла, по выходу команды, вообще не проверять)
Вот реальный шаг из плана:
{ "step_id": "create-server", "order": 2, "tool": "write_file", "input": { "path": "/workspace/index.js", "content": "const express = require('express');\nconst app = express();\n\napp.get('/health', (_, res) => res.json({ ok: true }));\napp.post('/echo', (req, res) => res.json(req.body));\n\nmodule.exports = app;" }, "determinism": "guaranteed", "verify": "hash_files_only", "description": "Create Express server with /health and /echo endpoints" }
Заметьте: "determinism": "guaranteed". Это значит: при повторном выполнении файл должен быть побайтово идентичен. Если нет — это дрейф, и система это увидит.
А вот шаг установки зависимостей:
{ "step_id": "install-deps", "order": 1, "tool": "run_command", "input": { "command": "npm", "args": ["ci"] }, "determinism": "best_effort", "verify": "hash_structured_output", "description": "Install pinned dependencies from lockfile" }
"determinism": "best_effort" — система знает, что npm ci может дать немного другой node_modules в зависимости от среды. Это нормально. При воспроизведении она не будет паниковать из-за этого, а проверит то, что действительно важно.
А теперь — самая важная часть: проверки
План без проверок — это просто скрипт с метаданными. Ничего особенного.
Проверки (assertions) — вот что превращает его в контракт.
{ "assertions": [ { "assertion_id": "tests-pass", "type": "exit_code", "description": "All tests must pass", "spec": { "command": "npm", "args": ["test"], "expected": 0 }, "required": true, "stability": "stable" }, { "assertion_id": "health-endpoint", "type": "http_response", "description": "Health endpoint returns 200", "spec": { "method": "GET", "url": "http://localhost:3000/health", "expected_status": 200, "body_contains": "\"ok\":true", "startup_command": "node index.js", "startup_timeout_ms": 5000, "shutdown_after": true }, "required": true, "stability": "flaky", "retry": { "max_attempts": 3, "backoff_ms": 2000 } } ] }
Теперь план говорит не «выполни шаги». Он говорит: «выполни шаги, и потом докажи, что результат правильный».
Тесты проходят? Эндпоинт отвечает? Тогда — и только тогда — результат «верифицирован».
Обратите внимание на "stability": "flaky" у HTTP-проверки. Система знает, что сеть может тупить, порт может быть занят, сервер может стартовать с задержкой. Поэтому она повторит попытку три раза с интервалом в 2 секунды. Без паники. Без вызова AI.
Два слоя верификации (это ключевое)
Вот здесь начинается то, ради чего стоило всё это строить.
У результата выполнения есть два независимых свойства:
Целостность (integrity): хэши файлов совпадают с оригиналом. Побайтовое совпадение. Физика.
Корректность (correctness): проверки проходят. Код делает то, что должен. Семантика.
Это разные оси. И их комбинация даёт четыре состояния:
Хэши совпали | Хэши разошлись | |
|---|---|---|
Проверки прошли | Идентично. Идеал. | Безобидный дрейф. |
Проверки упали | Регрессия. | Дрейф. Нужен ремонт. |
Средняя правая ячейка — безобидный дрейф (benign drift) — это самая важная вещь во всей системе.
Допустим, Express выпустил патч. package-lock.json изменился. Хэш node_modules другой. В мире v2.1 это было бы красным «DIVERGED». Паника. Ошибка.
Но в реальности — тесты проходят. Эндпоинт отвечает 200. Код работает правильно.
Система видит: целостность нарушена (хэши другие), но корректность сохранена (проверки прошли). Значит — мир двинулся вперёд, а результат всё ещё правильный. Можно принять новую реальность.
И вот тут критически важный момент: «принять» не значит «тихо подменить хэши в старом плане». Это значит — создать новое поколение плана. Новый файл. С новыми хэшами. С полной родословной, привязанной к предыдущему. Оригинал не тронут.
Это как Git: вы не меняете старый коммит. Вы создаёте новый.
Большая часть дрейфов — безобидные. Они решаются вот так. Без вызова AI. Бесплатно.
Когда дрейф не безобидный
Тесты упали. Эндпоинт отвечает 500. Что-то реально сломалось.
И вот тут вместо «ERROR: DIVERGED» (и разводите руками) включается каскад ремонта.
Четыре уровня. От дешёвого к дорогому. Каждый следующий включается, только если предыдущий не помог.
Уровень 1: Повтор. Для нестабильных проверок. HTTP-эндпоинт ответил 500, но может, сервер не успел стартовать? Подождать 2 секунды. Повторить. Ещё раз. Если на третьей попытке 200 — всё хорошо. Никакого ремонта не было. Просто мир неидеален.
Стоимость: 0 рублей. 0 токенов.
Уровень 2: Механический ремонт. Зависимости уехали? rm -rf node_modules && npm ci. Таймаут? Увеличить лимит. Файл пропал? Восстановить из артефактов предыдущего запуска.
Это не AI. Это хардкоженные стратегии. if (drift.category === 'dependency') → npm ci. Скучно, надёжно, предсказуемо.
Стоимость: 0 рублей. 0 токенов.
Уровень 3: LLM как ремонтный компилятор. Только если уровни 1 и 2 не помогли. Только если дрейф помечен как «ремонтопригодный». Одна попытка. Максимум 3 изменения в плане. Бюджет: $0.50 и 30 секунд.
LLM получает не «почини всё», а структурированный запрос: вот план, вот что сломалось (конкретные diff-ы), вот ограничения. Почини минимально. Не трогай то, что работает. Не добавляй фич.
Стоимость: ~$0.03–0.50. Один раз.
Уровень 4: Не починить. Мажорная версия Node. Ф��ндаментальное изменение API. Песочница не может это исправить автоматически. Система говорит: вот что сломалось, вот почему, вот что нужно сделать руками. Человекочитаемый отчёт с конкретными diff-ами.
Из 10 типичных дрейфов примерно 6 решаются безобидным дрейфом (бесплатно), 2 — механическим ремонтом (бесплатно), 1 — LLM-ремонтом (дёшево), и 1 — руками (но с понятным объяснением).
Как это выглядит в терминале
Ниже — пример вывода (упрощённый, формат близкий к реальному).
$ continuum run "Express API: GET /health, POST /echo, jest+supertest, pinned versions" Planning via Claude... 6 steps Executing in sandbox... [1/6] write_file: package.json ✓ sha256:a3f8... [2/6] run_command: npm install ✓ exit:0 [3/6] write_file: index.js ✓ sha256:7bc1... [4/6] write_file: index.test.js ✓ sha256:e4d2... [5/6] run_command: npm test ✓ exit:0 [6/6] write_file: README.md ✓ sha256:91fa... Assertions: ✓ tests-pass (exit_code: 0) ✓ health-endpoint (http: 200, attempt 1/3) ✓ echo-endpoint (http: 200, attempt 1/3) Status: VERIFIED Run: abc-123 Fingerprint: sha256:dd41...
Это первый запуск. AI спланировал. Песочница выполнила. Проверки прошли. Верифицировано.
Месяц спустя:
$ continuum replay abc-123 Fingerprint check: ⚠ node 20.11.0 → 20.12.0, lockfile hash changed Executing from saved plan (no LLM)... [1/6] write_file: package.json ✓ sha256:a3f8... (match) [2/6] run_command: npm install ✓ exit:0 [3/6] write_file: index.js ✓ sha256:7bc1... (match) [4/6] write_file: index.test.js ✓ sha256:e4d2... (match) [5/6] run_command: npm test ✗ exit:1 [6/6] write_file: README.md ✓ sha256:91fa... (match) Assertions: ✗ tests-pass (exit_code: 1, expected: 0) ✗ health-endpoint (http: 500, expected: 200) Drift detected: 2 vectors DEPENDENCY (degraded): express resolved 4.21.3, expected 4.21.2 ASSERTION_STABLE (blocking): tests-pass failed Verdict: DRIFTED Hint: run with --heal to attempt repair
Система не сказала «ОШИБКА». Она сказала: вот что именно сдвинулось, вот какой шаг затронут, вот серьёзность. И предложила чинить.
$ continuum replay abc-123 --heal Repair Cascade: Level 1: Retry (no flaky assertions to retry)... skipped Level 2: Deterministic repair... Strategy: npm-clean-install Action: rm -rf node_modules && npm ci Re-executing steps 2,5... Re-asserting... ✓ tests-pass (exit_code: 0) ✓ health-endpoint (http: 200, attempt 1/3) ✓ echo-endpoint (http: 200, attempt 1/3) Level 2: SUCCESS ✓ HEALED — generation 1 saved Mutation: deterministic (npm-clean-install) LLM calls: 0 Cost: $0.00
Я помню момент, когда увидел это в первый раз. Красный тест, два drift vector-а в логе — и я на автомате набрал --heal, не особо веря. Пять секунд. npm ci. Перезапуск. Зелёное.
Никакого AI. Никаких токенов. Cost: $0.00.
Система сама поняла, что express уехал, сама решила переустановить зависимости из lockfile, сама проверила, что после этого тесты проходят и эндпоинт отвечает 200. И сохранила результат как новое поколение плана. Оригинал не тронут.
Это был тот момент, когда я понял, что это не утилита. Это другой слой инфраструктуры.
Чем Continuum НЕ является
Не “автофикс всего мира” и не замена обновлений зависимостей.
Не “агент, который сам себе ставит задачи и бесконечно чинит код”.
Не замена тестов: без assertions инструмент бессмысленен.
Не очередной workflow engine: ценность в верификации, дрейф-диагнозе и каскаде ремонта.
«Подождите, а чем это отличается от Makefile?»
Этот вопрос я себе задавал раз двадцать. Потому что на поверхности — да, это план выполнения, как Makefile. Или как Dockerfile. Или как Ansible playbook.
Разница — в трёх вещах, которых нет ни у кого из них.
Первое: семантическая верификация. Makefile проверяет, выполнились ли команды. Continuum проверяет, правильный ли результат. npm test прошёл? Эндпоинт отвечает? Файл содержит то, что нужно? Это контракт на результат, а не на процесс.
Второе: классификация дрейфа. Когда Makefile ломается, вы получаете exit code 1. Когда Continuum обнаруживает расхождение, он говорит: это дрейф зависимостей, серьёзность — средняя, ремонтопригоден, стратегия — npm ci. Это не ошибка. Это диагноз.
Третье: ремонт. Makefile не чинит себя. Docker не чинит себя. CI не чинит себя. Continuum пробует починить механически, и только если не получается — вызывает AI с жёсткими ограничениями. Один раз. С бюджетом.
Если нарисовать стек:
Git — что изменилось в коде Docker — где код запускается CI/CD — когда запускать проверки Continuum — зачем код был создан и доказательство что он работает
Это другой слой. Между намерением и результатом. Слой, которого раньше не было, потому что код всегда писал человек, и намерение хранилось у него в голове.
Когда код пишет AI, намерение теряется в закрытом чат-окне. Continuum делает его артефактом.
Поколения планов
Каждый ремонт создаёт новое поколение. Не перезаписывает старое. Каждое поколение знает своего родителя.
$ continuum history sha256:a3f8 gen 0: sha256:a3f8 (original, claude-sonnet, 15 Jan) └─ gen 1: sha256:7bc1 (benign_drift: lockfile changed, 28 Jan) └─ gen 2: sha256:e4d2 (deterministic: npm-ci, 25 Feb) └─ gen 3: sha256:91fa (llm-repair: 1 mutation, 15 Mar)
Это не список патчей. Это родословная. Каждое поколение — полный, самодостаточный план, который можно выполнить независимо. С полным контекстом: кто его породил, почему, что изменилось.
Через год можно посмотреть на эту цепочку и понять: вот оригинальный замысел AI, вот как мир менялся, вот как система адаптировалась. Полный аудит. Полная прозрачность.
Что мне сказали, когда я показал это коллегам
«Зачем такое сложное, если можно просто запустить ещё раз?»
Можно. Если вам не нужно:
Знать, почему сломалось
Знать, что именно изменилось в мире
Починить без AI (бесплатно)
Доказать, что результат тот же
Дать коллеге воспроизводимый артефакт вместо промпта
«Это не будет работать для сложных проектов»
Возможно. Сейчас — да, это MVP, и он работает для относительно простых задач: API, библиотеки, CLI-утилиты, scaffold-ы. Но механизм масштабируется. Проверки можно сделать любой сложности. Стратегии ремонта можно добавлять.
«Это просто обёртка над Docker + скриптами»
Нет. Docker запускает код в изолированной среде. Continuum запускает намерение в изолированной среде, проверяет семантический результат, и чинит, когда среда дрейфует. Docker не знает, правильный ли результат. Continuum — знает.
Честные ограничения
Без этого раздела статья была бы враньём. Вот что реально плохо:
LLM-ремонт ненадёжен. Уровень 3 (LLM repair) работает в простых случаях: изменился import path, обновилась версия пакета. Для сложных семантических поломок AI часто не может починить с одной попытки. Сознательное ограничение: одна попытка, и если не помогло — честный отчёт.
Проверки нужно придумывать. Система хороша настолько, насколько хороши её assertions. Если AI сгенерировал план без нормальных проверок — benign drift и repair cascade бесполезны. Пока что это жёстко зависит от того, насколько хорошие assertions сгенерировал планировщик.
Только sequential execution. Шаги выполняются строго по порядку. Для простых проектов это нормально. Для монорепо с параллельными сборками — нет. Это осознанное ограничение MVP.
Docker обязателен. Без Docker изоляция не работает. Это зависимость, которую не все готовы принять.
Не работает для UI/frontend. Assertions вроде «кнопка выглядит правильно» или «анимация плавная» — за пределами текущих возможностей. Это runtime для backend-генерации, скриптов, инфраструктуры.
Экспериментальный статус. Это не production-ready система. Это working proof of concept, который показывает, что подход работает. Крайние случаи не покрыты. Документация неполная. API может измениться.
Почему open source
Потому что я не могу проверить эту идею один.
Если подход к воспроизводимости AI-кода правильный — он должен выдержать столкновение с реальным миром. С чужими проектами. С чужими средами. С крайними случаями, которые я не предусмотрел.
Если подход неправильный — я хочу узнать об этом до того, как потрачу на него год.
Всё на GitHub. MIT лицензия. TypeScript, Node.js, Docker. 8 из 10 CLI-команд не обращаются к AI.
npm install -g continuum-runtime # Первый запуск — AI планирует и выполняет continuum run "Express API: GET /health, POST /echo, jest tests, pinned deps" # Повтор без AI — доказательство воспроизводимости continuum replay <run_id> # Через месяц — когда мир сдвинулся continuum replay <run_id> --heal
Что дальше
Я начал с раздражения: код от Claude сломался, и мне лень переделывать. Обычная инженерная лень.
В процессе я понял несколько вещей, которые не ожидал:
Что CI/CD в текущем виде не готов к миру, где код пишет AI. Он проверяет артефакт, но не проверяет путь к нему.
Что «запусти ещё раз» — это не решение, а капитуляция. Каждый повторный вызов AI — это признание, что мы не контролируем результат.
Что разница между «хэши совпали» и «результат правильный» — это не педантизм, а фундаментальное архитектурное решение, на котором держится всё остальное.
Что в 9 случаях из 10 для починки не нужен AI. Нужна механика.
Гипотеза простая: AI-код должен стать воспроизводимым артефактом, а не одноразовым выводом модели.
Через год каждый второй проект в индустрии будет генерироваться AI-агентами. Вопрос «как это воспроизвести и починить, когда мир изменится» перестанет быть академическим.
Я просто хотел, чтобы мой Express API не умирал через месяц. Пришлось задуматься о том, что такое программа, если её написал не человек.
Кому это зайдёт
Если ты генеришь сервисы/CLI через Claude/Cursor и они умирают через недели.
Если тебе нужен аудит: что изменилось, где и почему.
Если ты хочешь повторять запуск без токенов, когда задача уже решена.
Обратная связь
Если хочешь разнести идею — лучше через issue/PR: мне реально важно понять, где подход ломается.
