Показывает, как код исполнялся на самом деле: какие функции звались, с какими доводами, что вернули и что бросили.
Инструмент дописывает в исходник запись о вызовах. Программа запускается как обычно, и каждый вызов оставляет две строки JSON — на входе и на выходе. Дальше эти строки читают глазами, фильтруют командой или отдают ИИ-агенту.
Языки: Python, JavaScript/TypeScript, C, C++, Elixir, Go, Java, C#. Схема записи у всех одна.
uv tool install git+https://github.com/digitable-lol/ouroborosInstalled 2 executables: ouroboros, ouroboros-mcp
Через Homebrew — одной строкой, Python доставится сам:
brew install digitable-lol/tap/ouroborosЛюбой из этих двух способов ставит ouroboros и ouroboros-mcp. Ещё есть asdf,
сборка из исходников, один файл-программа и образ — Установка;
там же подключение к Claude Code и Cursor как сервера MCP.
Возьмём обычный файл stats.py:
"""Средняя длительность запросов из журнала."""
def parse_line(line):
name, _, ms = line.partition(" ")
return name, int(ms)
def average(values):
return sum(values) / len(values)
def report(lines):
pairs = [parse_line(l) for l in lines]
return average([ms for _, ms in pairs])
if __name__ == "__main__":
print(report(["get 12", "put 30", "get 18"]))
print(report([]))ouroboros wrap-file stats.py{"ok": true, "path": "stats.py", "language": "python", "functions_wrapped": 3, "runtime_header": "ouroboros_runtime.py"}Файл после этого — тот же самый, плюс три строки с @_ouro_log и одна строка
ввоза. Ни отступы, ни комментарии, ни строка описания модуля не тронуты:
"""Средняя длительность запросов из журнала."""
from ouroboros_runtime import log as _ouro_log
@_ouro_log
def parse_line(line):
name, _, ms = line.partition(" ")
return name, int(ms)
@_ouro_log
def average(values):
return sum(values) / len(values)
@_ouro_log
def report(lines):
pairs = [parse_line(l) for l in lines]
return average([ms for _, ms in pairs])
if __name__ == "__main__":
print(report(["get 12", "put 30", "get 18"]))
print(report([]))Рядом появился ouroboros_runtime.py — тот самый помощник, который пишет
записи. Он ввозится только из стандартной библиотеки, ставить ничего не нужно.
python3 stats.py20.0
Traceback (most recent call last):
File "/srv/tmp/ouro-work/demo/stats.py", line 24, in <module>
print(report([]))
~~~~~~^^^^
File "/srv/tmp/ouro-work/demo/ouroboros_runtime.py", line 234, in wrapper
result = fn(*args, **kwargs)
File "/srv/tmp/ouro-work/demo/stats.py", line 19, in report
return average([ms for _, ms in pairs])
File "/srv/tmp/ouro-work/demo/ouroboros_runtime.py", line 234, in wrapper
result = fn(*args, **kwargs)
File "/srv/tmp/ouro-work/demo/stats.py", line 13, in average
return sum(values) / len(values)
~~~~~~~~~~~~^~~~~~~~~~~~~
ZeroDivisionError: division by zero
Возвращено то же и брошено то же, что и до обмазки. Рядом появился файл
debug.info.
Так не всегда. Обмазка — это правка исходника, и она умеет менять поведение программы, а не только время её работы. Проверенные случаи — в Границах. Прогоняйте проверки после обмазки, а не только до неё.
Что было брошено и с какими доводами:
ouroboros trace debug.info --outcome raised{
"ok": true,
"path": "debug.info",
"calls_parsed": 7,
"malformed": 0,
"matched": 2,
"returned": 2,
"next_cursor": null,
"in_flight": [],
"in_flight_truncated": false,
"records": [
{
"index": 5,
"started": "2026-08-28T23:39:45.166",
"call_id": "ef71eb89-6727-4cdf-a3b7-2ea54cff81e3",
"name": "average",
"args": "[]",
"kwargs": "",
"outcome_kind": "raised",
"outcome": "ZeroDivisionError: division by zero",
"duration": 3e-06,
"cpu": null,
"thread": "2864987.129949101195776"
},
{
"index": 6,
"started": "2026-08-28T23:39:45.166",
"call_id": "cb4e33f4-d0b2-4099-bcf2-044e4a88c1fa",
"name": "report",
"args": "[]",
"kwargs": "",
"outcome_kind": "raised",
"outcome": "ZeroDivisionError: division by zero",
"duration": 6.8e-05,
"cpu": null,
"thread": "2864987.129949101195776"
}
]
}Здесь видно то, чего нет в отслеживании стека: average позвали с пустым
списком, и пришёл он туда из report, которого позвали тоже с пустым. Не
«где сломалось», а «с чем позвали».
Сводка по всем вызовам сразу:
ouroboros trace-stats debug.info "by_function": [
{ "name": "parse_line", "count": 3, "result": 3, "raised": 0, "unknown": 0,
"duration_seconds": { "min": 1e-06, "max": 2e-06, "mean": 2e-06, "total": 5e-06, "count": 3 } },
{ "name": "average", "count": 2, "result": 1, "raised": 1, "unknown": 0,
"duration_seconds": { "min": 2e-06, "max": 3e-06, "mean": 2e-06, "total": 5e-06, "count": 2 } },
{ "name": "report", "count": 2, "result": 1, "raised": 1, "unknown": 0,
"duration_seconds": { "min": 6.8e-05, "max": 0.000346, "mean": 0.000207, "total": 0.000414, "count": 2 } }
],(вывод сокращён: показаны только by_function; целиком он в
Как это выглядит)
Всё, что выше, — вывод настоящих прогонов на обычной машине с Linux, Python 3.12.13. Пути в отслеживании стека — из того каталога, где прогон делался.
Только дописываемый файл, по одному объекту JSON в строке. Две строки на
вызов, связанные общим id:
{"p":"in","t":"2026-08-28T23:39:45.166","id":"e668ee33-…","ci":-1,"th":"2864987.129949101195776","fn":"average","a":"[12, 30, 18]","k":""}
{"p":"out","id":"e668ee33-…","fn":"average","r":"20.0","d":2e-06}| ключ | где | что |
|---|---|---|
p |
обе | in — вошли в вызов, out — вышли (вернули или бросили) |
t |
in |
время входа |
id |
обе | номер вызова; по нему строки и связываются |
ci |
in |
номер ядра процессора; -1, если недоступен |
th |
in |
поток: <номер процесса>.<номер потока> |
fn |
обе | имя функции (у C++ и Go — с именем класса или типа) |
a |
in |
позиционные доводы, снятые до выполнения тела |
k |
in |
именованные доводы |
r |
out |
что вернули |
x |
out |
что бросили: <Тип: сообщение>; взаимоисключается с r |
d |
out |
сколько шёл вызов, в секундах |
Отсюда самый дешёвый ответ на вопрос «где висит»: строка входа без парной
строки выхода — вызов вошёл и не вернулся. Такие вызовы trace и trace-stats
складывают в in_flight отдельно.
Полный разбор ключей и решений — SPEC.md.
Горячий файл целиком обмазывать не надо — нужные записи утонут. Есть выбор по именам:
ouroboros wrap-functions parser.c parse_header parse_bodyОтдельный черновик, если портить рабочее дерево не хочется: create заводит
каталог с историей, write дописывает записи при сохранении, execute
запускает и сам подставляет путь к debug.info, finish переносит результат
в соседний каталог.
ouroboros create /srv/tmp/разбор
ouroboros write /srv/tmp/разбор stats.py < stats.py
ouroboros execute /srv/tmp/разбор -- python3 stats.pyКод, который не разбирается, не сохраняется. write в этом случае
отказывает, а не кладёт полуобмазанный файл.
Все 17 команд — ouroboros --help; разбор по шагам —
Начало работы.
«Я не понимаю, что тут происходит.» Чужой проект, легаси, вчерашняя ошибка, которая не воспроизводится. Прочитать сорок тысяч строк дорого; прогнать и посмотреть, какие функции живые, с чем их зовут и что они возвращают, — дёшево.
«ИИ пишет мне код и не понимает, как он исполняется.» Модель, читающая
исходник, рассуждает о том, что должно произойти. По трассе видно, какие
ветви исполнялись, какие доводы встретились, какие вызовы бросили исключение.
Инструмент умеет работать сервером MCP — ouroboros-mcp, 17 инструментов.
→ Чтобы ИИ понимал, как код исполняется
Трасса записывает, как код себя вёл, а не как он должен себя вести.
Программа с ошибкой даёт трассу, в которой ошибка выглядит нормой. Это не оговорка внизу страницы, а устройство: Границы.
Ещё одно, измеренное на стенде: на задачах, где ошибка целиком видна по итоговому выводу, запись о вызовах не даёт ничего, и агент к ней не тянется — 0 из 3 прогонов. Отрицательный результат вместе с точной границей — bench/RESULTS.md.
| каталог | что там |
|---|---|
ouroboros/ |
сам пакет: командная строка, сервер MCP, черновик, языки |
tests/ |
проверок: 999 |
bench/ |
стенд и его итоги |
packaging/ |
один файл-программа, образ, формула Homebrew, плагин asdf |
docs/ |
страницы, они же опубликованы |
design/ |
исходное задание и разбор способа обмазки по языкам |
SPEC.md |
договор о формате записи, общий для восьми языков |
ARCHITECTURE.md |
как устроено внутри и как добавить язык |
Собрать и проверить у себя:
git clone https://github.com/digitable-lol/ouroboros && cd ouroboros
uv sync
scripts/qa.sh # ruff, mypy, pytest| страница | о чём |
|---|---|
| Установка | uv, Homebrew, asdf, из исходников, подключение сервера MCP |
| Начало работы | все команды, порядок работы, на чём спотыкаются |
| Прологировать чужой код | по шагам: что делать, что видишь, как читать |
| Чтобы ИИ понимал, как код исполняется | сервер MCP и его 17 инструментов |
| Обычная разработка | молчащая программа, регрессия, чего не стоит делать |
| Языки | восемь языков, чем они отличаются в записи |
| Границы | чего инструмент не делает и почему это осознанно |
| В чём смысл | зачем нужен и какую работу снимает |
| Как это выглядит | записи, сводка, настройка — целиком |
Те же страницы опубликованы: https://digitable-lol.github.io/ouroboros/.
Версия 0.5.0. Проверки: 999 из 999, покрытие с ветвями 100 % (ruff, mypy --strict, pytest). Установка
проверена целиком, а не «по виду правильно»: uv tool install;
brew install digitable-lol/tap/ouroboros вместе с brew test — короткая строка
подключает хранилище формул сама, проверено с пустого места: хранилище отцеплено,
прежняя версия снята, поставлено заново; asdf plugin add вместе с
asdf install 0.5.0 на чистом каталоге данных. После каждой поставленный
инструмент обмазывал настоящие файлы на Java и на C#, они собирались,
запускались, давали тот же вывод, что до обмазки, побайтово, и записи читались
этим же инструментом.
Все восемь языков прогнаны по отдельности.
Одинаковость поведения проверяется, а не предполагается. 151 программа на
восьми языках прогоняется дважды — без обмазки и с обмазкой — и три исхода
сверяются: вывод, код возврата, исключение. Расхождений было 16 из 79, стало 0. Случаи, которые
здесь раньше числились сломанными (пропажа "use strict" в JavaScript,
несобираемый return {1, 2, 3} в C++), закрыты и проверены прогоном.
Инструмент обмазывает сам себя: 23 файла собственного исходника, 248 функций, ни одной поломки разбора, и обмазанная копия проходит 990 проверок из 991. Единственная упавшая упала не от поломки: проверка даёт обмазанному инструменту прочитать файл записей и тем же путём велит ему писать свои записи туда же, так что он дописывает в него два собственных вызова и насчитывает пять вместо трёх. Числа сняты прогоном на этом дереве, а не переписаны с прошлого раза.
Что остаётся платой, а не поломкой. C++ не записывает возвращённый объект классового типа — иначе теряется пропуск копирования, который язык гарантирует. Имён доводов нет в записи ни у одного языка. Обмазка Python добавляет кадр стека на вызов, поэтому глубокая рекурсия мельчает вдвое. Каждое — с замером и объяснением в Границах.
BSD 2-Clause — полный текст в LICENSE.
Навык skill/SKILL.md — под той же лицензией.